diff --git a/dev/tools/gen_defaults/generated/used_tokens.csv b/dev/tools/gen_defaults/generated/used_tokens.csv index 544757e2b5..0dad3f3665 100644 --- a/dev/tools/gen_defaults/generated/used_tokens.csv +++ b/dev/tools/gen_defaults/generated/used_tokens.csv @@ -145,6 +145,13 @@ md.comp.elevated-button.label-text.text-style, md.comp.elevated-button.pressed.container.elevation, md.comp.elevated-button.pressed.state-layer.color, md.comp.elevated-button.pressed.state-layer.opacity, +md.comp.elevated-button.with-icon.disabled.icon.color, +md.comp.elevated-button.with-icon.disabled.icon.opacity, +md.comp.elevated-button.with-icon.focus.icon.color, +md.comp.elevated-button.with-icon.hover.icon.color, +md.comp.elevated-button.with-icon.icon.color, +md.comp.elevated-button.with-icon.icon.size, +md.comp.elevated-button.with-icon.pressed.icon.color, md.comp.elevated-card.container.color, md.comp.elevated-card.container.elevation, md.comp.elevated-card.container.shadow-color, @@ -198,6 +205,13 @@ md.comp.filled-button.label-text.text-style, md.comp.filled-button.pressed.container.elevation, md.comp.filled-button.pressed.state-layer.color, md.comp.filled-button.pressed.state-layer.opacity, +md.comp.filled-button.with-icon.disabled.icon.color, +md.comp.filled-button.with-icon.disabled.icon.opacity, +md.comp.filled-button.with-icon.focus.icon.color, +md.comp.filled-button.with-icon.hover.icon.color, +md.comp.filled-button.with-icon.icon.color, +md.comp.filled-button.with-icon.icon.size, +md.comp.filled-button.with-icon.pressed.icon.color, md.comp.filled-card.container.color, md.comp.filled-card.container.elevation, md.comp.filled-card.container.shadow-color, @@ -296,6 +310,13 @@ md.comp.filled-tonal-button.label-text.text-style, md.comp.filled-tonal-button.pressed.container.elevation, md.comp.filled-tonal-button.pressed.state-layer.color, md.comp.filled-tonal-button.pressed.state-layer.opacity, +md.comp.filled-tonal-button.with-icon.disabled.icon.color, +md.comp.filled-tonal-button.with-icon.disabled.icon.opacity, +md.comp.filled-tonal-button.with-icon.focus.icon.color, +md.comp.filled-tonal-button.with-icon.hover.icon.color, +md.comp.filled-tonal-button.with-icon.icon.color, +md.comp.filled-tonal-button.with-icon.icon.size, +md.comp.filled-tonal-button.with-icon.pressed.icon.color, md.comp.filled-tonal-icon-button.container.color, md.comp.filled-tonal-icon-button.container.height, md.comp.filled-tonal-icon-button.container.shape, @@ -405,6 +426,7 @@ md.comp.list.list-item.hover.state-layer.opacity, md.comp.list.list-item.label-text.color, md.comp.list.list-item.label-text.text-style, md.comp.list.list-item.leading-icon.color, +md.comp.list.list-item.leading-icon.size, md.comp.list.list-item.pressed.label-text.color, md.comp.list.list-item.pressed.leading-icon.icon.color, md.comp.list.list-item.pressed.state-layer.color, @@ -470,6 +492,13 @@ md.comp.outlined-button.outline.color, md.comp.outlined-button.outline.width, md.comp.outlined-button.pressed.state-layer.color, md.comp.outlined-button.pressed.state-layer.opacity, +md.comp.outlined-button.with-icon.disabled.icon.color, +md.comp.outlined-button.with-icon.disabled.icon.opacity, +md.comp.outlined-button.with-icon.focus.icon.color, +md.comp.outlined-button.with-icon.hover.icon.color, +md.comp.outlined-button.with-icon.icon.color, +md.comp.outlined-button.with-icon.icon.size, +md.comp.outlined-button.with-icon.pressed.icon.color, md.comp.outlined-card.container.color, md.comp.outlined-card.container.elevation, md.comp.outlined-card.container.shadow-color, @@ -703,6 +732,13 @@ md.comp.text-button.label-text.color, md.comp.text-button.label-text.text-style, md.comp.text-button.pressed.state-layer.color, md.comp.text-button.pressed.state-layer.opacity, +md.comp.text-button.with-icon.disabled.icon.color, +md.comp.text-button.with-icon.disabled.icon.opacity, +md.comp.text-button.with-icon.focus.icon.color, +md.comp.text-button.with-icon.hover.icon.color, +md.comp.text-button.with-icon.icon.color, +md.comp.text-button.with-icon.icon.size, +md.comp.text-button.with-icon.pressed.icon.color, md.comp.time-picker.clock-dial.color, md.comp.time-picker.clock-dial.container.size, md.comp.time-picker.clock-dial.label-text.text-style, diff --git a/dev/tools/gen_defaults/lib/button_template.dart b/dev/tools/gen_defaults/lib/button_template.dart index dc29fb26f0..4fcc6ff7de 100644 --- a/dev/tools/gen_defaults/lib/button_template.dart +++ b/dev/tools/gen_defaults/lib/button_template.dart @@ -125,6 +125,29 @@ class _${blockName}DefaultsM3 extends ButtonStyle { // No default fixedSize + @override + MaterialStateProperty? get iconSize => + const MaterialStatePropertyAll(${getToken("$tokenGroup.with-icon.icon.size")}); + + @override + MaterialStateProperty? get iconColor { + return MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return ${color('$tokenGroup.with-icon.disabled.icon.color')}.withOpacity(${opacity("$tokenGroup.with-icon.disabled.icon.opacity")}); + } + if (states.contains(MaterialState.pressed)) { + return ${color('$tokenGroup.with-icon.pressed.icon.color')}; + } + if (states.contains(MaterialState.hovered)) { + return ${color('$tokenGroup.with-icon.hover.icon.color')}; + } + if (states.contains(MaterialState.focused)) { + return ${color('$tokenGroup.with-icon.focus.icon.color')}; + } + return ${color('$tokenGroup.with-icon.icon.color')}; + }); + } + @override MaterialStateProperty? get maximumSize => const MaterialStatePropertyAll(Size.infinite); diff --git a/dev/tools/gen_defaults/lib/menu_template.dart b/dev/tools/gen_defaults/lib/menu_template.dart index aaef8459af..b471d865b4 100644 --- a/dev/tools/gen_defaults/lib/menu_template.dart +++ b/dev/tools/gen_defaults/lib/menu_template.dart @@ -121,6 +121,11 @@ class _MenuButtonDefaultsM3 extends ButtonStyle { // No default fixedSize + @override + MaterialStateProperty? get iconSize { + return const MaterialStatePropertyAll(${getToken("md.comp.list.list-item.leading-icon.size")}); + } + @override MaterialStateProperty? get maximumSize { return ButtonStyleButton.allOrNull(Size.infinite); diff --git a/packages/flutter/lib/src/material/button_style_button.dart b/packages/flutter/lib/src/material/button_style_button.dart index 748d02890c..8b5d52f49d 100644 --- a/packages/flutter/lib/src/material/button_style_button.dart +++ b/packages/flutter/lib/src/material/button_style_button.dart @@ -162,22 +162,39 @@ abstract class ButtonStyleButton extends StatefulWidget { /// {@macro flutter.material.ButtonStyleButton.iconAlignment} final IconAlignment iconAlignment; - /// Returns a non-null [ButtonStyle] that's based primarily on the [Theme]'s - /// [ThemeData.textTheme] and [ThemeData.colorScheme]. + /// Returns a [ButtonStyle] that's based primarily on the [Theme]'s + /// [ThemeData.textTheme] and [ThemeData.colorScheme], but has most values + /// filled out (non-null). /// - /// The returned style can be overridden by the [style] parameter and - /// by the style returned by [themeStyleOf]. For example the default - /// style of the [TextButton] subclass can be overridden with its - /// [TextButton.style] constructor parameter, or with a - /// [TextButtonTheme]. + /// The returned style can be overridden by the [style] parameter and by the + /// style returned by [themeStyleOf] that some button-specific themes like + /// [TextButtonTheme] or [ElevatedButtonTheme] override. For example the + /// default style of the [TextButton] subclass can be overridden with its + /// [TextButton.style] constructor parameter, or with a [TextButtonTheme]. /// - /// Concrete button subclasses should return a ButtonStyle that - /// has no null properties, and where all of the [WidgetStateProperty] - /// properties resolve to non-null values. + /// Concrete button subclasses should return a [ButtonStyle] with as many + /// non-null properties as possible, where all of the non-null + /// [WidgetStateProperty] properties resolve to non-null values. + /// + /// ## Properties that can be null + /// + /// Some properties, like [ButtonStyle.fixedSize] would override other values + /// in the same [ButtonStyle] if set, so they are allowed to be null. Here is + /// a summary of properties that are allowed to be null when returned in the + /// [ButtonStyle] returned by this function, an why: + /// + /// - [ButtonStyle.fixedSize] because it would override other values in the + /// same [ButtonStyle], like [ButtonStyle.maximumSize]. + /// - [ButtonStyle.side] because null is a valid value for a button that has + /// no side. [OutlinedButton] returns a non-null default for this, however. + /// - [ButtonStyle.backgroundBuilder] and [ButtonStyle.foregroundBuilder] + /// because they would override the [ButtonStyle.foregroundColor] and + /// [ButtonStyle.backgroundColor] of the same [ButtonStyle]. /// /// See also: /// - /// * [themeStyleOf], Returns the ButtonStyle of this button's component theme. + /// * [themeStyleOf], returns the ButtonStyle of this button's component + /// theme. @protected ButtonStyle defaultStyleOf(BuildContext context); @@ -479,7 +496,10 @@ class _ButtonStyleState extends State with TickerProviderStat customBorder: resolvedShape.copyWith(side: resolvedSide), statesController: statesController, child: IconTheme.merge( - data: IconThemeData(color: resolvedIconColor ?? resolvedForegroundColor, size: resolvedIconSize), + data: IconThemeData( + color: resolvedIconColor ?? resolvedForegroundColor, + size: resolvedIconSize, + ), child: effectiveChild, ), ), diff --git a/packages/flutter/lib/src/material/elevated_button.dart b/packages/flutter/lib/src/material/elevated_button.dart index bcaeae510e..f5e4cb52d5 100644 --- a/packages/flutter/lib/src/material/elevated_button.dart +++ b/packages/flutter/lib/src/material/elevated_button.dart @@ -698,6 +698,29 @@ class _ElevatedButtonDefaultsM3 extends ButtonStyle { // No default fixedSize + @override + MaterialStateProperty? get iconSize => + const MaterialStatePropertyAll(18.0); + + @override + MaterialStateProperty? get iconColor { + return MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return _colors.onSurface.withOpacity(0.38); + } + if (states.contains(MaterialState.pressed)) { + return _colors.primary; + } + if (states.contains(MaterialState.hovered)) { + return _colors.primary; + } + if (states.contains(MaterialState.focused)) { + return _colors.primary; + } + return _colors.primary; + }); + } + @override MaterialStateProperty? get maximumSize => const MaterialStatePropertyAll(Size.infinite); diff --git a/packages/flutter/lib/src/material/filled_button.dart b/packages/flutter/lib/src/material/filled_button.dart index 1ae47b75d2..6d0a1f225e 100644 --- a/packages/flutter/lib/src/material/filled_button.dart +++ b/packages/flutter/lib/src/material/filled_button.dart @@ -730,6 +730,29 @@ class _FilledButtonDefaultsM3 extends ButtonStyle { // No default fixedSize + @override + MaterialStateProperty? get iconSize => + const MaterialStatePropertyAll(18.0); + + @override + MaterialStateProperty? get iconColor { + return MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return _colors.onSurface.withOpacity(0.38); + } + if (states.contains(MaterialState.pressed)) { + return _colors.onPrimary; + } + if (states.contains(MaterialState.hovered)) { + return _colors.onPrimary; + } + if (states.contains(MaterialState.focused)) { + return _colors.onPrimary; + } + return _colors.onPrimary; + }); + } + @override MaterialStateProperty? get maximumSize => const MaterialStatePropertyAll(Size.infinite); @@ -852,6 +875,29 @@ class _FilledTonalButtonDefaultsM3 extends ButtonStyle { // No default fixedSize + @override + MaterialStateProperty? get iconSize => + const MaterialStatePropertyAll(18.0); + + @override + MaterialStateProperty? get iconColor { + return MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return _colors.onSurface.withOpacity(0.38); + } + if (states.contains(MaterialState.pressed)) { + return _colors.onSecondaryContainer; + } + if (states.contains(MaterialState.hovered)) { + return _colors.onSecondaryContainer; + } + if (states.contains(MaterialState.focused)) { + return _colors.onSecondaryContainer; + } + return _colors.onSecondaryContainer; + }); + } + @override MaterialStateProperty? get maximumSize => const MaterialStatePropertyAll(Size.infinite); diff --git a/packages/flutter/lib/src/material/menu_anchor.dart b/packages/flutter/lib/src/material/menu_anchor.dart index 0da73f1bd7..5eb3e1615d 100644 --- a/packages/flutter/lib/src/material/menu_anchor.dart +++ b/packages/flutter/lib/src/material/menu_anchor.dart @@ -3800,6 +3800,11 @@ class _MenuButtonDefaultsM3 extends ButtonStyle { // No default fixedSize + @override + MaterialStateProperty? get iconSize { + return const MaterialStatePropertyAll(24.0); + } + @override MaterialStateProperty? get maximumSize { return ButtonStyleButton.allOrNull(Size.infinite); diff --git a/packages/flutter/lib/src/material/outlined_button.dart b/packages/flutter/lib/src/material/outlined_button.dart index 404db919f8..72eab40131 100644 --- a/packages/flutter/lib/src/material/outlined_button.dart +++ b/packages/flutter/lib/src/material/outlined_button.dart @@ -606,6 +606,29 @@ class _OutlinedButtonDefaultsM3 extends ButtonStyle { // No default fixedSize + @override + MaterialStateProperty? get iconSize => + const MaterialStatePropertyAll(18.0); + + @override + MaterialStateProperty? get iconColor { + return MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return _colors.onSurface.withOpacity(0.38); + } + if (states.contains(MaterialState.pressed)) { + return _colors.primary; + } + if (states.contains(MaterialState.hovered)) { + return _colors.primary; + } + if (states.contains(MaterialState.focused)) { + return _colors.primary; + } + return _colors.primary; + }); + } + @override MaterialStateProperty? get maximumSize => const MaterialStatePropertyAll(Size.infinite); diff --git a/packages/flutter/lib/src/material/text_button.dart b/packages/flutter/lib/src/material/text_button.dart index 3dae185d6b..7d53b34eb8 100644 --- a/packages/flutter/lib/src/material/text_button.dart +++ b/packages/flutter/lib/src/material/text_button.dart @@ -639,6 +639,29 @@ class _TextButtonDefaultsM3 extends ButtonStyle { // No default fixedSize + @override + MaterialStateProperty? get iconSize => + const MaterialStatePropertyAll(18.0); + + @override + MaterialStateProperty? get iconColor { + return MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return _colors.onSurface.withOpacity(0.38); + } + if (states.contains(MaterialState.pressed)) { + return _colors.primary; + } + if (states.contains(MaterialState.hovered)) { + return _colors.primary; + } + if (states.contains(MaterialState.focused)) { + return _colors.primary; + } + return _colors.primary; + }); + } + @override MaterialStateProperty? get maximumSize => const MaterialStatePropertyAll(Size.infinite); diff --git a/packages/flutter/lib/src/material/toggle_buttons.dart b/packages/flutter/lib/src/material/toggle_buttons.dart index f1daf3fd51..d082bb8f12 100644 --- a/packages/flutter/lib/src/material/toggle_buttons.dart +++ b/packages/flutter/lib/src/material/toggle_buttons.dart @@ -775,6 +775,8 @@ class ToggleButtons extends StatelessWidget { style: ButtonStyle( backgroundColor: MaterialStatePropertyAll(effectiveFillColor), foregroundColor: MaterialStatePropertyAll(currentColor), + iconSize: const MaterialStatePropertyAll(24.0), + iconColor: MaterialStatePropertyAll(currentColor), overlayColor: _ToggleButtonDefaultOverlay( selected: onPressed != null && isSelected[index], unselected: onPressed != null && !isSelected[index], diff --git a/packages/flutter/test/material/button_style_test.dart b/packages/flutter/test/material/button_style_test.dart index 9b698729f3..04c99236f3 100644 --- a/packages/flutter/test/material/button_style_test.dart +++ b/packages/flutter/test/material/button_style_test.dart @@ -22,26 +22,30 @@ void main() { test('ButtonStyle defaults', () { const ButtonStyle style = ButtonStyle(); - expect(style.textStyle, null); - expect(style.backgroundColor, null); - expect(style.foregroundColor, null); - expect(style.overlayColor, null); - expect(style.shadowColor, null); - expect(style.surfaceTintColor, null); - expect(style.elevation, null); - expect(style.padding, null); - expect(style.minimumSize, null); - expect(style.fixedSize, null); - expect(style.maximumSize, null); - expect(style.iconColor, null); - expect(style.iconSize, null); - expect(style.side, null); - expect(style.shape, null); - expect(style.mouseCursor, null); - expect(style.visualDensity, null); - expect(style.tapTargetSize, null); - expect(style.animationDuration, null); - expect(style.enableFeedback, null); + expect(style.textStyle, isNull); + expect(style.backgroundColor, isNull); + expect(style.foregroundColor, isNull); + expect(style.overlayColor, isNull); + expect(style.shadowColor, isNull); + expect(style.surfaceTintColor, isNull); + expect(style.elevation, isNull); + expect(style.padding, isNull); + expect(style.minimumSize, isNull); + expect(style.fixedSize, isNull); + expect(style.maximumSize, isNull); + expect(style.iconColor, isNull); + expect(style.iconSize, isNull); + expect(style.side, isNull); + expect(style.shape, isNull); + expect(style.mouseCursor, isNull); + expect(style.visualDensity, isNull); + expect(style.tapTargetSize, isNull); + expect(style.animationDuration, isNull); + expect(style.enableFeedback, isNull); + expect(style.alignment, isNull); + expect(style.splashFactory, isNull); + expect(style.backgroundBuilder, isNull); + expect(style.foregroundBuilder, isNull); }); testWidgets('Default ButtonStyle debugFillProperties', (WidgetTester tester) async { diff --git a/packages/flutter/test/material/elevated_button_test.dart b/packages/flutter/test/material/elevated_button_test.dart index d6627b5d94..65c9b63b48 100644 --- a/packages/flutter/test/material/elevated_button_test.dart +++ b/packages/flutter/test/material/elevated_button_test.dart @@ -159,6 +159,114 @@ void main() { expect(material.type, MaterialType.button); }); + testWidgets('ElevatedButton.defaultStyle produces a ButtonStyle with appropriate non-null values', (WidgetTester tester) async { + const ColorScheme colorScheme = ColorScheme.light(); + final ThemeData theme = ThemeData.from(colorScheme: colorScheme); + + final ElevatedButton button = ElevatedButton( + onPressed: () { }, + child: const Text('button'), + ); + BuildContext? capturedContext; + // Enabled ElevatedButton + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: Center( + child: Builder( + builder: (BuildContext context) { + capturedContext = context; + return button; + } + ), + ), + ), + ); + final ButtonStyle style = button.defaultStyleOf(capturedContext!); + + // Properties that must be non-null. + expect(style.textStyle, isNotNull, reason: 'textStyle style'); + expect(style.backgroundColor, isNotNull, reason: 'backgroundColor style'); + expect(style.foregroundColor, isNotNull, reason: 'foregroundColor style'); + expect(style.overlayColor, isNotNull, reason: 'overlayColor style'); + expect(style.shadowColor, isNotNull, reason: 'shadowColor style'); + expect(style.surfaceTintColor, isNotNull, reason: 'surfaceTintColor style'); + expect(style.elevation, isNotNull, reason: 'elevation style'); + expect(style.padding, isNotNull, reason: 'padding style'); + expect(style.minimumSize, isNotNull, reason: 'minimumSize style'); + expect(style.maximumSize, isNotNull, reason: 'maximumSize style'); + expect(style.iconColor, isNotNull, reason: 'iconColor style'); + expect(style.iconSize, isNotNull, reason: 'iconSize style'); + expect(style.shape, isNotNull, reason: 'shape style'); + expect(style.mouseCursor, isNotNull, reason: 'mouseCursor style'); + expect(style.visualDensity, isNotNull, reason: 'visualDensity style'); + expect(style.tapTargetSize, isNotNull, reason: 'tapTargetSize style'); + expect(style.animationDuration, isNotNull, reason: 'animationDuration style'); + expect(style.enableFeedback, isNotNull, reason: 'enableFeedback style'); + expect(style.alignment, isNotNull, reason: 'alignment style'); + expect(style.splashFactory, isNotNull, reason: 'splashFactory style'); + + // Properties that are expected to be null. + expect(style.fixedSize, isNull, reason: 'fixedSize style'); + expect(style.side, isNull, reason: 'side style'); + expect(style.backgroundBuilder, isNull, reason: 'backgroundBuilder style'); + expect(style.foregroundBuilder, isNull, reason: 'foregroundBuilder style'); + }); + + testWidgets('ElevatedButton.defaultStyle with an icon produces a ButtonStyle with appropriate non-null values', (WidgetTester tester) async { + const ColorScheme colorScheme = ColorScheme.light(); + final ThemeData theme = ThemeData.from(colorScheme: colorScheme); + + final ElevatedButton button = ElevatedButton.icon( + onPressed: () { }, + icon: const SizedBox(), + label: const Text('button'), + ); + BuildContext? capturedContext; + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: Center( + child: Builder( + builder: (BuildContext context) { + capturedContext = context; + return button; + } + ), + ), + ), + ); + final ButtonStyle style = button.defaultStyleOf(capturedContext!); + + // Properties that must be non-null. + expect(style.textStyle, isNotNull, reason: 'textStyle style'); + expect(style.backgroundColor, isNotNull, reason: 'backgroundColor style'); + expect(style.foregroundColor, isNotNull, reason: 'foregroundColor style'); + expect(style.overlayColor, isNotNull, reason: 'overlayColor style'); + expect(style.shadowColor, isNotNull, reason: 'shadowColor style'); + expect(style.surfaceTintColor, isNotNull, reason: 'surfaceTintColor style'); + expect(style.elevation, isNotNull, reason: 'elevation style'); + expect(style.padding, isNotNull, reason: 'padding style'); + expect(style.minimumSize, isNotNull, reason: 'minimumSize style'); + expect(style.maximumSize, isNotNull, reason: 'maximumSize style'); + expect(style.iconColor, isNotNull, reason: 'iconColor style'); + expect(style.iconSize, isNotNull, reason: 'iconSize style'); + expect(style.shape, isNotNull, reason: 'shape style'); + expect(style.mouseCursor, isNotNull, reason: 'mouseCursor style'); + expect(style.visualDensity, isNotNull, reason: 'visualDensity style'); + expect(style.tapTargetSize, isNotNull, reason: 'tapTargetSize style'); + expect(style.animationDuration, isNotNull, reason: 'animationDuration style'); + expect(style.enableFeedback, isNotNull, reason: 'enableFeedback style'); + expect(style.alignment, isNotNull, reason: 'alignment style'); + expect(style.splashFactory, isNotNull, reason: 'splashFactory style'); + + // Properties that are expected to be null. + expect(style.fixedSize, isNull, reason: 'fixedSize style'); + expect(style.side, isNull, reason: 'side style'); + expect(style.backgroundBuilder, isNull, reason: 'backgroundBuilder style'); + expect(style.foregroundBuilder, isNull, reason: 'foregroundBuilder style'); + }); + testWidgets('ElevatedButton.icon produces the correct widgets if icon is null', (WidgetTester tester) async { const ColorScheme colorScheme = ColorScheme.light(); final ThemeData theme = ThemeData.from(colorScheme: colorScheme); @@ -409,6 +517,7 @@ void main() { data: ElevatedButtonThemeData( style: ButtonStyle( foregroundColor: MaterialStateProperty.resolveWith(getTextColor), + iconColor: MaterialStateProperty.resolveWith(getTextColor), ), ), child: Builder( @@ -995,8 +1104,8 @@ void main() { expect(paddingRect.left, iconRect.left - 16); // Use the taller widget to check the top and bottom padding. final Rect tallerWidget = iconRect.height > labelRect.height ? iconRect : labelRect; - expect(paddingRect.top, tallerWidget.top - 5); - expect(paddingRect.bottom, tallerWidget.bottom + 12); + expect(paddingRect.top, closeTo(tallerWidget.top - 6.5, .01)); + expect(paddingRect.bottom, closeTo(tallerWidget.bottom + 13.5, .01)); }); group('Default ElevatedButton padding for textScaleFactor, textDirection', () { diff --git a/packages/flutter/test/material/filled_button_test.dart b/packages/flutter/test/material/filled_button_test.dart index ad7f766e19..5d8b559efb 100644 --- a/packages/flutter/test/material/filled_button_test.dart +++ b/packages/flutter/test/material/filled_button_test.dart @@ -127,6 +127,114 @@ void main() { await tester.pumpAndSettle(); }); + testWidgets('FilledButton.defaultStyle produces a ButtonStyle with appropriate non-null values', (WidgetTester tester) async { + const ColorScheme colorScheme = ColorScheme.light(); + final ThemeData theme = ThemeData.from(colorScheme: colorScheme); + + final FilledButton button = FilledButton( + onPressed: () { }, + child: const Text('button'), + ); + BuildContext? capturedContext; + // Enabled FilledButton + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: Center( + child: Builder( + builder: (BuildContext context) { + capturedContext = context; + return button; + } + ), + ), + ), + ); + final ButtonStyle style = button.defaultStyleOf(capturedContext!); + + // Properties that must be non-null. + expect(style.textStyle, isNotNull, reason: 'textStyle style'); + expect(style.backgroundColor, isNotNull, reason: 'backgroundColor style'); + expect(style.foregroundColor, isNotNull, reason: 'foregroundColor style'); + expect(style.overlayColor, isNotNull, reason: 'overlayColor style'); + expect(style.shadowColor, isNotNull, reason: 'shadowColor style'); + expect(style.surfaceTintColor, isNotNull, reason: 'surfaceTintColor style'); + expect(style.elevation, isNotNull, reason: 'elevation style'); + expect(style.padding, isNotNull, reason: 'padding style'); + expect(style.minimumSize, isNotNull, reason: 'minimumSize style'); + expect(style.maximumSize, isNotNull, reason: 'maximumSize style'); + expect(style.iconColor, isNotNull, reason: 'iconColor style'); + expect(style.iconSize, isNotNull, reason: 'iconSize style'); + expect(style.shape, isNotNull, reason: 'shape style'); + expect(style.mouseCursor, isNotNull, reason: 'mouseCursor style'); + expect(style.visualDensity, isNotNull, reason: 'visualDensity style'); + expect(style.tapTargetSize, isNotNull, reason: 'tapTargetSize style'); + expect(style.animationDuration, isNotNull, reason: 'animationDuration style'); + expect(style.enableFeedback, isNotNull, reason: 'enableFeedback style'); + expect(style.alignment, isNotNull, reason: 'alignment style'); + expect(style.splashFactory, isNotNull, reason: 'splashFactory style'); + + // Properties that are expected to be null. + expect(style.fixedSize, isNull, reason: 'fixedSize style'); + expect(style.side, isNull, reason: 'side style'); + expect(style.backgroundBuilder, isNull, reason: 'backgroundBuilder style'); + expect(style.foregroundBuilder, isNull, reason: 'foregroundBuilder style'); + }); + + testWidgets('FilledButton.defaultStyle with an icon produces a ButtonStyle with appropriate non-null values', (WidgetTester tester) async { + const ColorScheme colorScheme = ColorScheme.light(); + final ThemeData theme = ThemeData.from(colorScheme: colorScheme); + + final FilledButton button = FilledButton.icon( + onPressed: () { }, + icon: const SizedBox(), + label: const Text('button'), + ); + BuildContext? capturedContext; + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: Center( + child: Builder( + builder: (BuildContext context) { + capturedContext = context; + return button; + } + ), + ), + ), + ); + final ButtonStyle style = button.defaultStyleOf(capturedContext!); + + // Properties that must be non-null. + expect(style.textStyle, isNotNull, reason: 'textStyle style'); + expect(style.backgroundColor, isNotNull, reason: 'backgroundColor style'); + expect(style.foregroundColor, isNotNull, reason: 'foregroundColor style'); + expect(style.overlayColor, isNotNull, reason: 'overlayColor style'); + expect(style.shadowColor, isNotNull, reason: 'shadowColor style'); + expect(style.surfaceTintColor, isNotNull, reason: 'surfaceTintColor style'); + expect(style.elevation, isNotNull, reason: 'elevation style'); + expect(style.padding, isNotNull, reason: 'padding style'); + expect(style.minimumSize, isNotNull, reason: 'minimumSize style'); + expect(style.maximumSize, isNotNull, reason: 'maximumSize style'); + expect(style.iconColor, isNotNull, reason: 'iconColor style'); + expect(style.iconSize, isNotNull, reason: 'iconSize style'); + expect(style.shape, isNotNull, reason: 'shape style'); + expect(style.mouseCursor, isNotNull, reason: 'mouseCursor style'); + expect(style.visualDensity, isNotNull, reason: 'visualDensity style'); + expect(style.tapTargetSize, isNotNull, reason: 'tapTargetSize style'); + expect(style.animationDuration, isNotNull, reason: 'animationDuration style'); + expect(style.enableFeedback, isNotNull, reason: 'enableFeedback style'); + expect(style.alignment, isNotNull, reason: 'alignment style'); + expect(style.splashFactory, isNotNull, reason: 'splashFactory style'); + + // Properties that are expected to be null. + expect(style.fixedSize, isNull, reason: 'fixedSize style'); + expect(style.side, isNull, reason: 'side style'); + expect(style.backgroundBuilder, isNull, reason: 'backgroundBuilder style'); + expect(style.foregroundBuilder, isNull, reason: 'foregroundBuilder style'); + }); + testWidgets('FilledButton.icon produces the correct widgets if icon is null', (WidgetTester tester) async { const ColorScheme colorScheme = ColorScheme.light(); final ThemeData theme = ThemeData.from(colorScheme: colorScheme); @@ -601,6 +709,7 @@ void main() { data: FilledButtonThemeData( style: ButtonStyle( foregroundColor: MaterialStateProperty.resolveWith(getTextColor), + iconColor: MaterialStateProperty.resolveWith(getTextColor), ), ), child: Builder( @@ -1174,14 +1283,18 @@ void main() { final Rect labelRect = tester.getRect(find.byKey(labelKey)); final Rect iconRect = tester.getRect(find.byType(Icon)); + Matcher closeOnWeb(num value) { + return kIsWeb ? closeTo(value, 1e-2) : equals(value); + } + // The right padding should be applied on the right of the label, whereas the // left padding should be applied on the left side of the icon. - expect(paddingRect.right, labelRect.right + 10); - expect(paddingRect.left, iconRect.left - 16); + expect(paddingRect.right, equals(labelRect.right + 10)); + expect(paddingRect.left, equals(iconRect.left - 16)); // Use the taller widget to check the top and bottom padding. final Rect tallerWidget = iconRect.height > labelRect.height ? iconRect : labelRect; - expect(paddingRect.top, tallerWidget.top - 5); - expect(paddingRect.bottom, tallerWidget.bottom + 12); + expect(paddingRect.top, closeOnWeb(tallerWidget.top - 6.5)); + expect(paddingRect.bottom, closeOnWeb(tallerWidget.bottom + 13.5)); }); group('Default FilledButton padding for textScaleFactor, textDirection', () { diff --git a/packages/flutter/test/material/outlined_button_test.dart b/packages/flutter/test/material/outlined_button_test.dart index e2a9235e87..48e80bc0e9 100644 --- a/packages/flutter/test/material/outlined_button_test.dart +++ b/packages/flutter/test/material/outlined_button_test.dart @@ -174,6 +174,114 @@ void main() { expect(material.type, MaterialType.button); }); + testWidgets('OutlinedButton.defaultStyle produces a ButtonStyle with appropriate non-null values', (WidgetTester tester) async { + const ColorScheme colorScheme = ColorScheme.light(); + final ThemeData theme = ThemeData.from(colorScheme: colorScheme); + + final OutlinedButton button = OutlinedButton( + onPressed: () { }, + child: const Text('button'), + ); + BuildContext? capturedContext; + // Enabled OutlinedButton + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: Center( + child: Builder( + builder: (BuildContext context) { + capturedContext = context; + return button; + } + ), + ), + ), + ); + final ButtonStyle style = button.defaultStyleOf(capturedContext!); + + // Properties that must be non-null. + expect(style.textStyle, isNotNull, reason: 'textStyle style'); + expect(style.backgroundColor, isNotNull, reason: 'backgroundColor style'); + expect(style.foregroundColor, isNotNull, reason: 'foregroundColor style'); + expect(style.overlayColor, isNotNull, reason: 'overlayColor style'); + expect(style.shadowColor, isNotNull, reason: 'shadowColor style'); + expect(style.surfaceTintColor, isNotNull, reason: 'surfaceTintColor style'); + expect(style.elevation, isNotNull, reason: 'elevation style'); + expect(style.padding, isNotNull, reason: 'padding style'); + expect(style.minimumSize, isNotNull, reason: 'minimumSize style'); + expect(style.maximumSize, isNotNull, reason: 'maximumSize style'); + expect(style.iconColor, isNotNull, reason: 'iconColor style'); + expect(style.iconSize, isNotNull, reason: 'iconSize style'); + expect(style.shape, isNotNull, reason: 'shape style'); + expect(style.mouseCursor, isNotNull, reason: 'mouseCursor style'); + expect(style.visualDensity, isNotNull, reason: 'visualDensity style'); + expect(style.tapTargetSize, isNotNull, reason: 'tapTargetSize style'); + expect(style.animationDuration, isNotNull, reason: 'animationDuration style'); + expect(style.enableFeedback, isNotNull, reason: 'enableFeedback style'); + expect(style.alignment, isNotNull, reason: 'alignment style'); + expect(style.splashFactory, isNotNull, reason: 'splashFactory style'); + expect(style.side, isNotNull, reason: 'side style'); + + // Properties that are expected to be null. + expect(style.fixedSize, isNull, reason: 'fixedSize style'); + expect(style.backgroundBuilder, isNull, reason: 'backgroundBuilder style'); + expect(style.foregroundBuilder, isNull, reason: 'foregroundBuilder style'); + }); + + testWidgets('OutlinedButton.defaultStyle with an icon produces a ButtonStyle with appropriate non-null values', (WidgetTester tester) async { + const ColorScheme colorScheme = ColorScheme.light(); + final ThemeData theme = ThemeData.from(colorScheme: colorScheme); + + final OutlinedButton button = OutlinedButton.icon( + onPressed: () { }, + icon: const SizedBox(), + label: const Text('button'), + ); + BuildContext? capturedContext; + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: Center( + child: Builder( + builder: (BuildContext context) { + capturedContext = context; + return button; + } + ), + ), + ), + ); + final ButtonStyle style = button.defaultStyleOf(capturedContext!); + + // Properties that must be non-null. + expect(style.textStyle, isNotNull, reason: 'textStyle style'); + expect(style.backgroundColor, isNotNull, reason: 'backgroundColor style'); + expect(style.foregroundColor, isNotNull, reason: 'foregroundColor style'); + expect(style.overlayColor, isNotNull, reason: 'overlayColor style'); + expect(style.shadowColor, isNotNull, reason: 'shadowColor style'); + expect(style.surfaceTintColor, isNotNull, reason: 'surfaceTintColor style'); + expect(style.elevation, isNotNull, reason: 'elevation style'); + expect(style.padding, isNotNull, reason: 'padding style'); + expect(style.minimumSize, isNotNull, reason: 'minimumSize style'); + expect(style.maximumSize, isNotNull, reason: 'maximumSize style'); + expect(style.iconColor, isNotNull, reason: 'iconColor style'); + expect(style.iconSize, isNotNull, reason: 'iconSize style'); + expect(style.shape, isNotNull, reason: 'shape style'); + expect(style.mouseCursor, isNotNull, reason: 'mouseCursor style'); + expect(style.visualDensity, isNotNull, reason: 'visualDensity style'); + expect(style.tapTargetSize, isNotNull, reason: 'tapTargetSize style'); + expect(style.animationDuration, isNotNull, reason: 'animationDuration style'); + expect(style.enableFeedback, isNotNull, reason: 'enableFeedback style'); + expect(style.alignment, isNotNull, reason: 'alignment style'); + expect(style.splashFactory, isNotNull, reason: 'splashFactory style'); + expect(style.side, isNotNull, reason: 'side style'); + + // Properties that are expected to be null. + expect(style.fixedSize, isNull, reason: 'fixedSize style'); + expect(style.backgroundBuilder, isNull, reason: 'backgroundBuilder style'); + expect(style.foregroundBuilder, isNull, reason: 'foregroundBuilder style'); + }); + testWidgets('OutlinedButton.icon produces the correct widgets if icon is null', (WidgetTester tester) async { const ColorScheme colorScheme = ColorScheme.light(); final ThemeData theme = ThemeData.from(colorScheme: colorScheme); @@ -600,7 +708,7 @@ void main() { child: OutlinedButton.icon( key: buttonKey, style: ButtonStyle( - foregroundColor: MaterialStateProperty.resolveWith(getIconColor), + iconColor: MaterialStateProperty.resolveWith(getIconColor), ), icon: const Icon(Icons.add), label: const Text('OutlinedButton'), diff --git a/packages/flutter/test/material/text_button_test.dart b/packages/flutter/test/material/text_button_test.dart index 640d9649f8..0452c28062 100644 --- a/packages/flutter/test/material/text_button_test.dart +++ b/packages/flutter/test/material/text_button_test.dart @@ -154,6 +154,114 @@ void main() { expect(material.type, MaterialType.button); }); + testWidgets('TextButton.defaultStyle produces a ButtonStyle with appropriate non-null values', (WidgetTester tester) async { + const ColorScheme colorScheme = ColorScheme.light(); + final ThemeData theme = ThemeData.from(colorScheme: colorScheme); + + final TextButton button = TextButton( + onPressed: () { }, + child: const Text('button'), + ); + BuildContext? capturedContext; + // Enabled TextButton + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: Center( + child: Builder( + builder: (BuildContext context) { + capturedContext = context; + return button; + } + ), + ), + ), + ); + final ButtonStyle style = button.defaultStyleOf(capturedContext!); + + // Properties that must be non-null. + expect(style.textStyle, isNotNull, reason: 'textStyle style'); + expect(style.backgroundColor, isNotNull, reason: 'backgroundColor style'); + expect(style.foregroundColor, isNotNull, reason: 'foregroundColor style'); + expect(style.overlayColor, isNotNull, reason: 'overlayColor style'); + expect(style.shadowColor, isNotNull, reason: 'shadowColor style'); + expect(style.surfaceTintColor, isNotNull, reason: 'surfaceTintColor style'); + expect(style.elevation, isNotNull, reason: 'elevation style'); + expect(style.padding, isNotNull, reason: 'padding style'); + expect(style.minimumSize, isNotNull, reason: 'minimumSize style'); + expect(style.maximumSize, isNotNull, reason: 'maximumSize style'); + expect(style.iconColor, isNotNull, reason: 'iconColor style'); + expect(style.iconSize, isNotNull, reason: 'iconSize style'); + expect(style.shape, isNotNull, reason: 'shape style'); + expect(style.mouseCursor, isNotNull, reason: 'mouseCursor style'); + expect(style.visualDensity, isNotNull, reason: 'visualDensity style'); + expect(style.tapTargetSize, isNotNull, reason: 'tapTargetSize style'); + expect(style.animationDuration, isNotNull, reason: 'animationDuration style'); + expect(style.enableFeedback, isNotNull, reason: 'enableFeedback style'); + expect(style.alignment, isNotNull, reason: 'alignment style'); + expect(style.splashFactory, isNotNull, reason: 'splashFactory style'); + + // Properties that are expected to be null. + expect(style.fixedSize, isNull, reason: 'fixedSize style'); + expect(style.side, isNull, reason: 'side style'); + expect(style.backgroundBuilder, isNull, reason: 'backgroundBuilder style'); + expect(style.foregroundBuilder, isNull, reason: 'foregroundBuilder style'); + }); + + testWidgets('TextButton.defaultStyle with an icon produces a ButtonStyle with appropriate non-null values', (WidgetTester tester) async { + const ColorScheme colorScheme = ColorScheme.light(); + final ThemeData theme = ThemeData.from(colorScheme: colorScheme); + + final TextButton button = TextButton.icon( + onPressed: () { }, + icon: const SizedBox(), + label: const Text('button'), + ); + BuildContext? capturedContext; + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: Center( + child: Builder( + builder: (BuildContext context) { + capturedContext = context; + return button; + } + ), + ), + ), + ); + final ButtonStyle style = button.defaultStyleOf(capturedContext!); + + // Properties that must be non-null. + expect(style.textStyle, isNotNull, reason: 'textStyle style'); + expect(style.backgroundColor, isNotNull, reason: 'backgroundColor style'); + expect(style.foregroundColor, isNotNull, reason: 'foregroundColor style'); + expect(style.overlayColor, isNotNull, reason: 'overlayColor style'); + expect(style.shadowColor, isNotNull, reason: 'shadowColor style'); + expect(style.surfaceTintColor, isNotNull, reason: 'surfaceTintColor style'); + expect(style.elevation, isNotNull, reason: 'elevation style'); + expect(style.padding, isNotNull, reason: 'padding style'); + expect(style.minimumSize, isNotNull, reason: 'minimumSize style'); + expect(style.maximumSize, isNotNull, reason: 'maximumSize style'); + expect(style.iconColor, isNotNull, reason: 'iconColor style'); + expect(style.iconSize, isNotNull, reason: 'iconSize style'); + expect(style.shape, isNotNull, reason: 'shape style'); + expect(style.mouseCursor, isNotNull, reason: 'mouseCursor style'); + expect(style.visualDensity, isNotNull, reason: 'visualDensity style'); + expect(style.tapTargetSize, isNotNull, reason: 'tapTargetSize style'); + expect(style.animationDuration, isNotNull, reason: 'animationDuration style'); + expect(style.enableFeedback, isNotNull, reason: 'enableFeedback style'); + expect(style.alignment, isNotNull, reason: 'alignment style'); + expect(style.splashFactory, isNotNull, reason: 'splashFactory style'); + + // Properties that are expected to be null. + expect(style.fixedSize, isNull, reason: 'fixedSize style'); + expect(style.side, isNull, reason: 'side style'); + expect(style.backgroundBuilder, isNull, reason: 'backgroundBuilder style'); + expect(style.foregroundBuilder, isNull, reason: 'foregroundBuilder style'); + }); + testWidgets('TextButton.icon produces the correct widgets when icon is null', (WidgetTester tester) async { const ColorScheme colorScheme = ColorScheme.light(); final ThemeData theme = ThemeData.from(colorScheme: colorScheme); @@ -463,6 +571,7 @@ void main() { child: TextButton.icon( style: ButtonStyle( foregroundColor: MaterialStateProperty.resolveWith(getTextColor), + iconColor: MaterialStateProperty.resolveWith(getTextColor), ), key: buttonKey, icon: const Icon(Icons.add),