Skip to content

Conversation

@TahaTesser
Copy link
Member

@TahaTesser TahaTesser commented Sep 9, 2024

Fixes Add missing icon props in button styleFrom methods.

Description

Add missing icon propers in the following widgets:

  • ElevatedButton.styleFrom (missing iconSize)
  • FilledButton.styleFrom (missing iconSize)
  • OutlinedButton.styleFrom (missing iconSize)
  • TextButton.styleFrom (missing iconSize)
  • MenuItemButton.styleFrom (missing iconSize and disabledIconColor)
  • SubmenuButton.styleFrom (missing iconSize and disabledIconColor)
  • SegmentedButton.styleFrom (missing iconSize, iconColor, and disabledIconColor)

Code sample

expand to view the code sample
import 'package:flutter/material.dart';

enum Calendar { day, week, month, year }

void main() => runApp(const MyApp());

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  Calendar calendarView = Calendar.week;
  bool isEnabled = true;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: Center(
          child: Column(
            spacing: 10.0,
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              ElevatedButton.icon(
                style: ElevatedButton.styleFrom(
                  iconSize: 30,
                  iconColor: Colors.red,
                  disabledIconColor: Colors.red.withValues(alpha: 0.5),
                ),
                onPressed: isEnabled ? () {} : null,
                icon: const Icon(Icons.add),
                label: const Text('ElevatedButton'),
              ),
              FilledButton.icon(
                style: ElevatedButton.styleFrom(
                  iconSize: 30,
                  iconColor: Colors.red,
                  disabledIconColor: Colors.red.withValues(alpha: 0.5),
                ),
                onPressed: isEnabled ? () {} : null,
                icon: const Icon(Icons.add),
                label: const Text('FilledButton'),
              ),
              FilledButton.tonalIcon(
                style: ElevatedButton.styleFrom(
                  iconSize: 30,
                  iconColor: Colors.red,
                  disabledIconColor: Colors.red.withValues(alpha: 0.5),
                ),
                onPressed: isEnabled ? () {} : null,
                icon: const Icon(Icons.add),
                label: const Text('Add'),
              ),
              OutlinedButton.icon(
                style: ElevatedButton.styleFrom(
                  iconSize: 30,
                  iconColor: Colors.red,
                  disabledIconColor: Colors.red.withValues(alpha: 0.5),
                ),
                onPressed: isEnabled ? () {} : null,
                icon: const Icon(Icons.add),
                label: const Text('OutlinedButton'),
              ),
              TextButton.icon(
                style: ElevatedButton.styleFrom(
                  iconSize: 30,
                  iconColor: Colors.red,
                  disabledIconColor: Colors.red.withValues(alpha: 0.5),
                ),
                onPressed: isEnabled ? () {} : null,
                icon: const Icon(Icons.add),
                label: const Text('TextButton'),
              ),
              SizedBox(
                width: 200,
                child: MenuItemButton(
                  style: MenuItemButton.styleFrom(
                    iconSize: 30,
                    iconColor: Colors.red,
                    disabledIconColor: Colors.red.withValues(alpha: 0.5),
                  ),
                  trailingIcon: const Icon(Icons.arrow_forward_ios),
                  onPressed: isEnabled ? () {} : null,
                  child: const Text('MenuItemButton'),
                ),
              ),
              SizedBox(
                width: 200,
                child: SubmenuButton(
                  style: SubmenuButton.styleFrom(
                    iconSize: 30,
                    iconColor: Colors.red,
                    disabledIconColor: Colors.red.withValues(alpha: 0.5),
                  ),
                  trailingIcon: const Icon(Icons.arrow_forward_ios),
                  menuChildren: <Widget>[
                    if (isEnabled) const Text('Item'),
                  ],
                  child: const Text('SubmenuButton'),
                ),
              ),
              SegmentedButton<Calendar>(
                style: SegmentedButton.styleFrom(
                  iconColor: Colors.red,
                  iconSize: 30,
                  disabledIconColor: Colors.red.withValues(alpha: 0.5),
                ),
                segments: const <ButtonSegment<Calendar>>[
                  ButtonSegment<Calendar>(
                      value: Calendar.day,
                      label: Text('Day'),
                      icon: Icon(Icons.calendar_view_day)),
                  ButtonSegment<Calendar>(
                      value: Calendar.week,
                      label: Text('Week'),
                      icon: Icon(Icons.calendar_view_week)),
                  ButtonSegment<Calendar>(
                      value: Calendar.month,
                      label: Text('Month'),
                      icon: Icon(Icons.calendar_view_month)),
                  ButtonSegment<Calendar>(
                      value: Calendar.year,
                      label: Text('Year'),
                      icon: Icon(Icons.calendar_today)),
                ],
                selected: <Calendar>{calendarView},
                onSelectionChanged:
                    isEnabled ? (Set<Calendar> newSelection) {} : null,
              )
            ],
          ),
        ),
        floatingActionButton: FloatingActionButton.extended(
          onPressed: () {
            setState(() {
              isEnabled = !isEnabled;
            });
          },
          label: Text(isEnabled ? 'Enabled' : 'Disabled'),
        ),
      ),
    );
  }
}

Preview (Customized using icon props in styleFrom methods)

Screenshot 2024-09-09 at 16 27 19

Pre-launch Checklist

If you need help, consider asking for advice on the #hackers-new channel on Discord.

@github-actions github-actions bot added a: text input Entering text in a text field or keyboard related problems framework flutter/packages/flutter repository. See also f: labels. f: material design flutter/packages/flutter/material repository. labels Sep 9, 2024
@TahaTesser TahaTesser marked this pull request as ready for review September 9, 2024 14:22
});

// This is a test regression for https://github.com/flutter/flutter/issues/154798.
test('ElevatedButton.styleFrom constructor', () {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a question, what is this unit test testing for?

Copy link
Member Author

@TahaTesser TahaTesser Sep 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This tests all the expected styleFrom method parameters. Some existing tests only verify some of the styleFrom properties but not every parameter hence we missed some icon properties.

This test is regular test not a widget test, just verifying props similar to debug fill properties tests.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if you create an additional ButtonStyle with the same parameters but with the normal ButtonStyle constructor, and then compare their equality? Not sure if that would work like I'm thinking it would, but if so it would fail if an additional parameter is added to ButtonStyle but not to styleFrom, which would be pretty valuable.

Copy link
Member Author

@TahaTesser TahaTesser Sep 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@justinmc Something like this?

  // Regression test for https://github.com/flutter/flutter/issues/154798.
  test('ElevatedButton.styleFrom constructor', () {
    const Color foregroundColor = Color(0xFF000022);
    const Color backgroundColor = Color(0xFF000011);
    const Color disabledForegroundColor = Color(0xFF000033);
    const Color disabledBackgroundColor = Color(0xFF000044);
    const Color shadowColor = Color(0xFF000066);
    const Color surfaceTintColor = Color(0xFF000077);
    const Color iconColor = Color(0xFF000088);
    const double iconSize = 32.0;
    const Color disabledIconColor = Color(0xFF000099);
    const Color overlayColor = Color(0xFF000055);
    const double elevation = 10.0;
    const TextStyle textStyle = TextStyle(fontSize: 21.0);
    const EdgeInsetsGeometry padding = EdgeInsets.all(16.0);
    const Size minimumSize = Size(88.0, 36.0);
    const Size fixedSize = Size(100.0, 50.0);
    const Size maximumSize = Size(120.0, 48.0);
    const BorderSide side = BorderSide(width: 2.0, color: Color(0xFF0000AA));
    final OutlinedBorder shape = RoundedRectangleBorder(borderRadius: BorderRadius.circular(8.0));
    const MouseCursor enabledMouseCursor = SystemMouseCursors.click;
    const MouseCursor disabledMouseCursor = SystemMouseCursors.basic;
    const VisualDensity visualDensity = VisualDensity.compact;
    const MaterialTapTargetSize tapTargetSize = MaterialTapTargetSize.shrinkWrap;
    const Duration animationDuration = Duration(milliseconds: 200);
    const bool enableFeedback = true;
    const AlignmentGeometry alignment = Alignment.center;
    const InteractiveInkFeatureFactory splashFactory = InkRipple.splashFactory;
    Widget backgroundBuilder(BuildContext context, Set<WidgetState> states, Widget? child) {
      return const SizedBox();
    }
    Widget foregroundBuilder(BuildContext context, Set<WidgetState> states, Widget? child) {
      return const SizedBox();
    }

    final ButtonStyle styleFrom = ElevatedButton.styleFrom(
      foregroundColor: foregroundColor,
      backgroundColor: backgroundColor,
      disabledForegroundColor: disabledForegroundColor,
      disabledBackgroundColor: disabledBackgroundColor,
      shadowColor: shadowColor,
      surfaceTintColor: surfaceTintColor,
      iconColor: iconColor,
      iconSize: iconSize,
      disabledIconColor: disabledIconColor,
      overlayColor: overlayColor,
      elevation: elevation,
      textStyle: textStyle,
      padding: padding,
      minimumSize: minimumSize,
      fixedSize: fixedSize,
      maximumSize: maximumSize,
      side: side,
      shape: shape,
      enabledMouseCursor: enabledMouseCursor,
      disabledMouseCursor: disabledMouseCursor,
      visualDensity: visualDensity,
      tapTargetSize: tapTargetSize,
      animationDuration: animationDuration,
      enableFeedback: enableFeedback,
      alignment: alignment,
      splashFactory: splashFactory,
      backgroundBuilder: backgroundBuilder,
      foregroundBuilder: foregroundBuilder,
    );

    final ButtonStyle buttonStyle = ButtonStyle(
      foregroundColor: ButtonStyleButton.defaultColor(foregroundColor, disabledForegroundColor),
      backgroundColor: ButtonStyleButton.defaultColor(backgroundColor, disabledBackgroundColor),
      shadowColor: const WidgetStatePropertyAll<Color?>(shadowColor),
      surfaceTintColor: const WidgetStatePropertyAll<Color?>(surfaceTintColor),
      iconColor: ButtonStyleButton.defaultColor(iconColor, disabledIconColor),
      iconSize: const WidgetStatePropertyAll<double>(iconSize),
      overlayColor: const WidgetStatePropertyAll<Color?>(overlayColor),
      elevation: const WidgetStatePropertyAll<double>(elevation),
      textStyle: const WidgetStatePropertyAll<TextStyle?>(textStyle),
      padding: const WidgetStatePropertyAll<EdgeInsetsGeometry>(padding),
      minimumSize: const WidgetStatePropertyAll<Size>(minimumSize),
      fixedSize: const WidgetStatePropertyAll<Size>(fixedSize),
      maximumSize: const WidgetStatePropertyAll<Size>(maximumSize),
      side: const WidgetStatePropertyAll<BorderSide>(side),
      shape: WidgetStatePropertyAll<OutlinedBorder>(shape),
      mouseCursor: const WidgetStateProperty<MouseCursor?>.fromMap(
        <WidgetStatesConstraint, MouseCursor?>{
          WidgetState.disabled: disabledMouseCursor,
          WidgetState.any: enabledMouseCursor,
        },
      ),
      visualDensity: visualDensity,
      tapTargetSize: tapTargetSize,
      animationDuration: animationDuration,
      enableFeedback: enableFeedback,
      alignment: alignment,
      splashFactory: splashFactory,
      backgroundBuilder: backgroundBuilder,
      foregroundBuilder: foregroundBuilder,
    );

    expect(
      styleFrom.foregroundColor!.resolve(<WidgetState>{ WidgetState.disabled }),
      buttonStyle.foregroundColor!.resolve(<WidgetState>{ WidgetState.disabled }),
    );
    expect(
      styleFrom.foregroundColor!.resolve(<WidgetState>{}),
      buttonStyle.foregroundColor!.resolve(<WidgetState>{}),
    );
    expect(
      styleFrom.backgroundColor!.resolve(<WidgetState>{ WidgetState.disabled }),
      buttonStyle.backgroundColor!.resolve(<WidgetState>{ WidgetState.disabled }),
    );
    expect(
      styleFrom.backgroundColor!.resolve(<WidgetState>{}),
      buttonStyle.backgroundColor!.resolve(<WidgetState>{}),
    );
    expect(
      styleFrom.shadowColor!.resolve(<WidgetState>{}),
      buttonStyle.shadowColor!.resolve(<WidgetState>{}),
    );
    expect(
      styleFrom.surfaceTintColor!.resolve(<WidgetState>{}),
      buttonStyle.surfaceTintColor!.resolve(<WidgetState>{}),
    );
    expect(
      styleFrom.iconColor!.resolve(<WidgetState>{ WidgetState.disabled }),
      buttonStyle.iconColor!.resolve(<WidgetState>{ WidgetState.disabled }),
    );
    expect(
      styleFrom.iconColor!.resolve(<WidgetState>{}),
      buttonStyle.iconColor!.resolve(<WidgetState>{}),
    );
    expect(
      styleFrom.iconSize!.resolve(<WidgetState>{}),
      buttonStyle.iconSize!.resolve(<WidgetState>{}),
    );
    expect(styleFrom.overlayColor!.resolve(<WidgetState>{}), null); // This is null when a foreground is provided.
    expect(
      styleFrom.elevation!.resolve(<WidgetState>{}),
      buttonStyle.elevation!.resolve(<WidgetState>{}),
    );
    expect(
      styleFrom.textStyle!.resolve(<WidgetState>{}),
      buttonStyle.textStyle!.resolve(<WidgetState>{}),
    );
    expect(
      styleFrom.padding!.resolve(<WidgetState>{}),
      buttonStyle.padding!.resolve(<WidgetState>{}),
    );
    expect(
      styleFrom.minimumSize!.resolve(<WidgetState>{}),
      buttonStyle.minimumSize!.resolve(<WidgetState>{}),
    );
    expect(
      styleFrom.fixedSize!.resolve(<WidgetState>{}),
      buttonStyle.fixedSize!.resolve(<WidgetState>{}),
    );
    expect(
      styleFrom.maximumSize!.resolve(<WidgetState>{}),
      buttonStyle.maximumSize!.resolve(<WidgetState>{}),
    );
    expect(
      styleFrom.side!.resolve(<WidgetState>{}),
      buttonStyle.side!.resolve(<WidgetState>{}),
    );
    expect(
      styleFrom.shape!.resolve(<WidgetState>{}),
      buttonStyle.shape!.resolve(<WidgetState>{}),
    );
    expect(
      styleFrom.mouseCursor!.resolve(<WidgetState>{ WidgetState.disabled }),
      buttonStyle.mouseCursor!.resolve(<WidgetState>{ WidgetState.disabled }),
    );
    expect(
      styleFrom.mouseCursor!.resolve(<WidgetState>{}),
      buttonStyle.mouseCursor!.resolve(<WidgetState>{}),
    );
    expect(styleFrom.visualDensity, buttonStyle.visualDensity);
    expect(styleFrom.tapTargetSize, buttonStyle.tapTargetSize);
    expect(styleFrom.animationDuration, buttonStyle.animationDuration);
    expect(styleFrom.enableFeedback, buttonStyle.enableFeedback);
    expect(styleFrom.alignment, buttonStyle.alignment);
    expect(styleFrom.splashFactory, buttonStyle.splashFactory);
    expect(styleFrom.backgroundBuilder, buttonStyle.backgroundBuilder);
    expect(styleFrom.foregroundBuilder, buttonStyle.foregroundBuilder);
  });

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My guess is that it would just need 1 equality check:

expect(styleFrom == buttonStyle, isTrue);

Though if we were to add a ButtonStyle property without updating this test, they would both be initialized to null and we likely wouldn't be able to catch that .styleFrom() was missing something.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expect(styleFrom == buttonStyle, isTrue); is same as expect(styleFrom, buttonStyle);.

I tried this already. Either checks won't work as the some of the individuals properties return different types. We will have to break down each properties like i did in the #154821 (comment).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I like this idea but seems this new change still can not catch newly added property in ButtonStyle? If so, probably we can just keep the original tests.

Further more, I'm just thinking whether these constructor tests are necessary because they don't have any expect()s and seems can only make sure .styleFrom method doesn't throw exception? xxButton.styleFrom is just to provide a more convenient way to customize buttons, so probably it is okay to have some mismatch properties with ButtonStyle, for example .stylefrom has foregroundColor and disabledForegroundColor but ButtonStyle only has foregroundColor with a WidgetStateProperty type.

Copy link
Member Author

@TahaTesser TahaTesser Sep 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Further more, I'm just thinking whether these constructor tests are necessary because they don't have any expect()s and seems can only make sure .styleFrom method doesn't throw exception?

The test doesn't expect to not throw exception, the test only use constructor in the same older as the implementation to not miss any props.

Sure, I can remove these constructor tests. We should make sure to sync all classes using styleFrom when new properties are introduced in styleFrom. As this PR adds missing styleFrom props in seven methods.

Copy link
Contributor

@nate-thegrate nate-thegrate left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks really great overall. I wanted to share a few tiny suggestions (and unfortunately they involve a lot of copy/pasting, so apologies for that).

@nate-thegrate
Copy link
Contributor

Also, apologies for the merge conflicts, that's on me 😅

(though to be fair, #154695 was opened before #154798 was)

@TahaTesser TahaTesser force-pushed the fix_icon_props_style_from_methods branch 2 times, most recently from d67597c to 4d077df Compare September 10, 2024 10:01
@TahaTesser
Copy link
Member Author

TahaTesser commented Sep 10, 2024

Also, apologies for the merge conflicts, that's on me 😅

(though to be fair, #154695 was opened before #154798 was)

FYI, I see no conflict in text_button.dart, only in other buttons. The iconColor line isn't updated in this class.

iconColor: ButtonStyleButton.defaultColor(iconColor, disabledIconColor),

Copy link
Contributor

@nate-thegrate nate-thegrate left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

There's a rich variety of improvements in this PR; looking forward to seeing it get merged in 🙂

});

// This is a test regression for https://github.com/flutter/flutter/issues/154798.
test('ElevatedButton.styleFrom constructor', () {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My guess is that it would just need 1 equality check:

expect(styleFrom == buttonStyle, isTrue);

Though if we were to add a ButtonStyle property without updating this test, they would both be initialized to null and we likely wouldn't be able to catch that .styleFrom() was missing something.

@TahaTesser TahaTesser requested a review from justinmc September 11, 2024 15:35
@TahaTesser TahaTesser force-pushed the fix_icon_props_style_from_methods branch from 4d077df to c867f46 Compare September 13, 2024 08:37
Copy link
Contributor

@QuncCccccc QuncCccccc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM:) Thanks!

@TahaTesser TahaTesser force-pushed the fix_icon_props_style_from_methods branch from c867f46 to d0fdad3 Compare September 17, 2024 20:50
@TahaTesser TahaTesser added the autosubmit Merge PR when tree becomes green via auto submit App label Sep 18, 2024
@auto-submit auto-submit bot merged commit d5e843e into flutter:master Sep 18, 2024
@TahaTesser TahaTesser deleted the fix_icon_props_style_from_methods branch September 18, 2024 00:51
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Sep 18, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Sep 18, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Sep 18, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Sep 18, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Sep 18, 2024
auto-submit bot pushed a commit to flutter/packages that referenced this pull request Sep 19, 2024
Manual roll requested by [email protected]

flutter/flutter@c4c9f47...7fe8237

2024-09-18 [email protected] Roll Flutter Engine from 4bdcbf39c200 to 15e9e3153266 (1 revision) (flutter/flutter#155371)
2024-09-18 [email protected] Roll Flutter Engine from fdf38910bad4 to 4bdcbf39c200 (5 revisions) (flutter/flutter#155368)
2024-09-18 [email protected] Roll Packages from 4f2b9cd to c7406b3 (1 revision) (flutter/flutter#155362)
2024-09-18 [email protected] Roll Flutter Engine from 4f2d866aef32 to fdf38910bad4 (1 revision) (flutter/flutter#155339)
2024-09-18 [email protected] Roll Flutter Engine from 311ba971bb3a to 4f2d866aef32 (4 revisions) (flutter/flutter#155329)
2024-09-18 [email protected] Fix missing icon props in button `styleFrom`  methods (flutter/flutter#154821)
2024-09-17 [email protected] Roll Flutter Engine from 0ef18a3ef064 to 311ba971bb3a (8 revisions) (flutter/flutter#155325)
2024-09-17 [email protected] [CupertinoAlertDialog] Add tap-slide gesture (flutter/flutter#154853)
2024-09-17 [email protected] Uninstall /can fail/ (flutter/flutter#155314)
2024-09-17 [email protected] Roll Packages from df88c81 to 4f2b9cd (3 revisions) (flutter/flutter#155312)
2024-09-17 [email protected] Roll Flutter Engine from a1700b9ea2db to 0ef18a3ef064 (1 revision) (flutter/flutter#155311)
2024-09-17 [email protected] Roll Flutter Engine from 1376288f5c2a to a1700b9ea2db (2 revisions) (flutter/flutter#155290)
2024-09-17 [email protected] Delete packages/flutter_tools/lib/src/fuchsia directory (flutter/flutter#154880)
2024-09-17 [email protected] Adds ColorSwatch matcher (flutter/flutter#155272)

If this roll has caused a breakage, revert this CL and stop the roller
using the controls here:
https://autoroll.skia.org/r/flutter-packages
Please CC [email protected],[email protected],[email protected] on the revert to ensure that a human
is aware of the problem.

To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://issues.skia.org/issues/new?component=1389291&template=1850622

Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Dec 11, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request Dec 12, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a: text input Entering text in a text field or keyboard related problems autosubmit Merge PR when tree becomes green via auto submit App f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add missing icon props in button styleFrom methods.

4 participants