Fix handling of iconSize and iconColor defaults for ButtonStyleButton subclasses. (#143501)

## Description

Adds defaults that use tokens to define default `iconSize` and `iconColor` values. Previously, the Material 3 token values for button icon sizes and colors were not being used as defaults when the `ButtonStyleButton.defaultStyleOf` function returned the default values.

Adds tests to make sure appropriate `ButtonStyle` fields are populated when defaultStyle is called on buttons.

Updated documentation for `defaultStyleOf` to indicated that not _all_ fields need to be non-null, since some fields make sense to be null (e.g. `fixedSize`) because they would otherwise override the behavior of other fields in the same `ButtonStyle`.

## Tests
 - Added tests to make sure that the appropriate fields are non-null in the default button styles for each type of button.
This commit is contained in:
Greg Spencer 2024-08-02 16:31:07 -07:00 committed by GitHub
parent 97996b07a2
commit 51ed348f3b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 688 additions and 39 deletions

View File

@ -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,

1 Versions used v4_1_0
145 md.comp.elevated-button.pressed.container.elevation
146 md.comp.elevated-button.pressed.state-layer.color
147 md.comp.elevated-button.pressed.state-layer.opacity
148 md.comp.elevated-button.with-icon.disabled.icon.color
149 md.comp.elevated-button.with-icon.disabled.icon.opacity
150 md.comp.elevated-button.with-icon.focus.icon.color
151 md.comp.elevated-button.with-icon.hover.icon.color
152 md.comp.elevated-button.with-icon.icon.color
153 md.comp.elevated-button.with-icon.icon.size
154 md.comp.elevated-button.with-icon.pressed.icon.color
155 md.comp.elevated-card.container.color
156 md.comp.elevated-card.container.elevation
157 md.comp.elevated-card.container.shadow-color
205 md.comp.filled-button.pressed.container.elevation
206 md.comp.filled-button.pressed.state-layer.color
207 md.comp.filled-button.pressed.state-layer.opacity
208 md.comp.filled-button.with-icon.disabled.icon.color
209 md.comp.filled-button.with-icon.disabled.icon.opacity
210 md.comp.filled-button.with-icon.focus.icon.color
211 md.comp.filled-button.with-icon.hover.icon.color
212 md.comp.filled-button.with-icon.icon.color
213 md.comp.filled-button.with-icon.icon.size
214 md.comp.filled-button.with-icon.pressed.icon.color
215 md.comp.filled-card.container.color
216 md.comp.filled-card.container.elevation
217 md.comp.filled-card.container.shadow-color
310 md.comp.filled-tonal-button.pressed.container.elevation
311 md.comp.filled-tonal-button.pressed.state-layer.color
312 md.comp.filled-tonal-button.pressed.state-layer.opacity
313 md.comp.filled-tonal-button.with-icon.disabled.icon.color
314 md.comp.filled-tonal-button.with-icon.disabled.icon.opacity
315 md.comp.filled-tonal-button.with-icon.focus.icon.color
316 md.comp.filled-tonal-button.with-icon.hover.icon.color
317 md.comp.filled-tonal-button.with-icon.icon.color
318 md.comp.filled-tonal-button.with-icon.icon.size
319 md.comp.filled-tonal-button.with-icon.pressed.icon.color
320 md.comp.filled-tonal-icon-button.container.color
321 md.comp.filled-tonal-icon-button.container.height
322 md.comp.filled-tonal-icon-button.container.shape
426 md.comp.list.list-item.label-text.color
427 md.comp.list.list-item.label-text.text-style
428 md.comp.list.list-item.leading-icon.color
429 md.comp.list.list-item.leading-icon.size
430 md.comp.list.list-item.pressed.label-text.color
431 md.comp.list.list-item.pressed.leading-icon.icon.color
432 md.comp.list.list-item.pressed.state-layer.color
492 md.comp.outlined-button.outline.width
493 md.comp.outlined-button.pressed.state-layer.color
494 md.comp.outlined-button.pressed.state-layer.opacity
495 md.comp.outlined-button.with-icon.disabled.icon.color
496 md.comp.outlined-button.with-icon.disabled.icon.opacity
497 md.comp.outlined-button.with-icon.focus.icon.color
498 md.comp.outlined-button.with-icon.hover.icon.color
499 md.comp.outlined-button.with-icon.icon.color
500 md.comp.outlined-button.with-icon.icon.size
501 md.comp.outlined-button.with-icon.pressed.icon.color
502 md.comp.outlined-card.container.color
503 md.comp.outlined-card.container.elevation
504 md.comp.outlined-card.container.shadow-color
732 md.comp.text-button.label-text.text-style
733 md.comp.text-button.pressed.state-layer.color
734 md.comp.text-button.pressed.state-layer.opacity
735 md.comp.text-button.with-icon.disabled.icon.color
736 md.comp.text-button.with-icon.disabled.icon.opacity
737 md.comp.text-button.with-icon.focus.icon.color
738 md.comp.text-button.with-icon.hover.icon.color
739 md.comp.text-button.with-icon.icon.color
740 md.comp.text-button.with-icon.icon.size
741 md.comp.text-button.with-icon.pressed.icon.color
742 md.comp.time-picker.clock-dial.color
743 md.comp.time-picker.clock-dial.container.size
744 md.comp.time-picker.clock-dial.label-text.text-style

View File

@ -125,6 +125,29 @@ class _${blockName}DefaultsM3 extends ButtonStyle {
// No default fixedSize
@override
MaterialStateProperty<double>? get iconSize =>
const MaterialStatePropertyAll<double>(${getToken("$tokenGroup.with-icon.icon.size")});
@override
MaterialStateProperty<Color>? get iconColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> 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<Size>? get maximumSize =>
const MaterialStatePropertyAll<Size>(Size.infinite);

View File

@ -121,6 +121,11 @@ class _MenuButtonDefaultsM3 extends ButtonStyle {
// No default fixedSize
@override
MaterialStateProperty<double>? get iconSize {
return const MaterialStatePropertyAll<double>(${getToken("md.comp.list.list-item.leading-icon.size")});
}
@override
MaterialStateProperty<Size>? get maximumSize {
return ButtonStyleButton.allOrNull<Size>(Size.infinite);

View File

@ -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<ButtonStyleButton> 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,
),
),

View File

@ -698,6 +698,29 @@ class _ElevatedButtonDefaultsM3 extends ButtonStyle {
// No default fixedSize
@override
MaterialStateProperty<double>? get iconSize =>
const MaterialStatePropertyAll<double>(18.0);
@override
MaterialStateProperty<Color>? get iconColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> 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<Size>? get maximumSize =>
const MaterialStatePropertyAll<Size>(Size.infinite);

View File

@ -730,6 +730,29 @@ class _FilledButtonDefaultsM3 extends ButtonStyle {
// No default fixedSize
@override
MaterialStateProperty<double>? get iconSize =>
const MaterialStatePropertyAll<double>(18.0);
@override
MaterialStateProperty<Color>? get iconColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> 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<Size>? get maximumSize =>
const MaterialStatePropertyAll<Size>(Size.infinite);
@ -852,6 +875,29 @@ class _FilledTonalButtonDefaultsM3 extends ButtonStyle {
// No default fixedSize
@override
MaterialStateProperty<double>? get iconSize =>
const MaterialStatePropertyAll<double>(18.0);
@override
MaterialStateProperty<Color>? get iconColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> 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<Size>? get maximumSize =>
const MaterialStatePropertyAll<Size>(Size.infinite);

View File

@ -3800,6 +3800,11 @@ class _MenuButtonDefaultsM3 extends ButtonStyle {
// No default fixedSize
@override
MaterialStateProperty<double>? get iconSize {
return const MaterialStatePropertyAll<double>(24.0);
}
@override
MaterialStateProperty<Size>? get maximumSize {
return ButtonStyleButton.allOrNull<Size>(Size.infinite);

View File

@ -606,6 +606,29 @@ class _OutlinedButtonDefaultsM3 extends ButtonStyle {
// No default fixedSize
@override
MaterialStateProperty<double>? get iconSize =>
const MaterialStatePropertyAll<double>(18.0);
@override
MaterialStateProperty<Color>? get iconColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> 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<Size>? get maximumSize =>
const MaterialStatePropertyAll<Size>(Size.infinite);

View File

@ -639,6 +639,29 @@ class _TextButtonDefaultsM3 extends ButtonStyle {
// No default fixedSize
@override
MaterialStateProperty<double>? get iconSize =>
const MaterialStatePropertyAll<double>(18.0);
@override
MaterialStateProperty<Color>? get iconColor {
return MaterialStateProperty.resolveWith((Set<MaterialState> 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<Size>? get maximumSize =>
const MaterialStatePropertyAll<Size>(Size.infinite);

View File

@ -775,6 +775,8 @@ class ToggleButtons extends StatelessWidget {
style: ButtonStyle(
backgroundColor: MaterialStatePropertyAll<Color?>(effectiveFillColor),
foregroundColor: MaterialStatePropertyAll<Color?>(currentColor),
iconSize: const MaterialStatePropertyAll<double>(24.0),
iconColor: MaterialStatePropertyAll<Color?>(currentColor),
overlayColor: _ToggleButtonDefaultOverlay(
selected: onPressed != null && isSelected[index],
unselected: onPressed != null && !isSelected[index],

View File

@ -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 {

View File

@ -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<Color>(getTextColor),
iconColor: MaterialStateProperty.resolveWith<Color>(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', () {

View File

@ -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<Color>(getTextColor),
iconColor: MaterialStateProperty.resolveWith<Color>(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', () {

View File

@ -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<Color>(getIconColor),
iconColor: MaterialStateProperty.resolveWith<Color>(getIconColor),
),
icon: const Icon(Icons.add),
label: const Text('OutlinedButton'),

View File

@ -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<Color>(getTextColor),
iconColor: MaterialStateProperty.resolveWith<Color>(getTextColor),
),
key: buttonKey,
icon: const Icon(Icons.add),