diff --git a/packages/flutter/lib/src/material/button_style.dart b/packages/flutter/lib/src/material/button_style.dart index a61a4e6994..487fd517ab 100644 --- a/packages/flutter/lib/src/material/button_style.dart +++ b/packages/flutter/lib/src/material/button_style.dart @@ -23,6 +23,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; +import 'button_style_button.dart'; import 'ink_well.dart'; import 'material_state.dart'; import 'theme_data.dart'; @@ -174,6 +175,7 @@ class ButtonStyle with Diagnosticable { this.maximumSize, this.iconColor, this.iconSize, + this.iconAlignment, this.side, this.shape, this.mouseCursor, @@ -279,6 +281,22 @@ class ButtonStyle with Diagnosticable { /// The icon's size inside of the button. final MaterialStateProperty? iconSize; + /// The alignment of the button's icon. + /// + /// This property is supported for the following button types: + /// + /// * [ElevatedButton.icon]. + /// * [FilledButton.icon]. + /// * [FilledButton.tonalIcon]. + /// * [OutlinedButton.icon]. + /// * [TextButton.icon]. + /// + /// See also: + /// + /// * [IconAlignment], for more information about the different icon + /// alignments. + final IconAlignment? iconAlignment; + /// The color and weight of the button's outline. /// /// This value is combined with [shape] to create a shape decorated @@ -407,6 +425,7 @@ class ButtonStyle with Diagnosticable { MaterialStateProperty? maximumSize, MaterialStateProperty? iconColor, MaterialStateProperty? iconSize, + IconAlignment? iconAlignment, MaterialStateProperty? side, MaterialStateProperty? shape, MaterialStateProperty? mouseCursor, @@ -433,6 +452,7 @@ class ButtonStyle with Diagnosticable { maximumSize: maximumSize ?? this.maximumSize, iconColor: iconColor ?? this.iconColor, iconSize: iconSize ?? this.iconSize, + iconAlignment: iconAlignment ?? this.iconAlignment, side: side ?? this.side, shape: shape ?? this.shape, mouseCursor: mouseCursor ?? this.mouseCursor, @@ -470,6 +490,7 @@ class ButtonStyle with Diagnosticable { maximumSize: maximumSize ?? style.maximumSize, iconColor: iconColor ?? style.iconColor, iconSize: iconSize ?? style.iconSize, + iconAlignment: iconAlignment ?? style.iconAlignment, side: side ?? style.side, shape: shape ?? style.shape, mouseCursor: mouseCursor ?? style.mouseCursor, @@ -500,6 +521,7 @@ class ButtonStyle with Diagnosticable { maximumSize, iconColor, iconSize, + iconAlignment, side, shape, mouseCursor, @@ -537,6 +559,7 @@ class ButtonStyle with Diagnosticable { && other.maximumSize == maximumSize && other.iconColor == iconColor && other.iconSize == iconSize + && other.iconAlignment == iconAlignment && other.side == side && other.shape == shape && other.mouseCursor == mouseCursor @@ -566,6 +589,7 @@ class ButtonStyle with Diagnosticable { properties.add(DiagnosticsProperty>('maximumSize', maximumSize, defaultValue: null)); properties.add(DiagnosticsProperty>('iconColor', iconColor, defaultValue: null)); properties.add(DiagnosticsProperty>('iconSize', iconSize, defaultValue: null)); + properties.add(EnumProperty('iconAlignment', iconAlignment, defaultValue: null)); properties.add(DiagnosticsProperty>('side', side, defaultValue: null)); properties.add(DiagnosticsProperty>('shape', shape, defaultValue: null)); properties.add(DiagnosticsProperty>('mouseCursor', mouseCursor, defaultValue: null)); @@ -597,6 +621,7 @@ class ButtonStyle with Diagnosticable { maximumSize: MaterialStateProperty.lerp(a?.maximumSize, b?.maximumSize, t, Size.lerp), iconColor: MaterialStateProperty.lerp(a?.iconColor, b?.iconColor, t, Color.lerp), iconSize: MaterialStateProperty.lerp(a?.iconSize, b?.iconSize, t, lerpDouble), + iconAlignment: t < 0.5 ? a?.iconAlignment : b?.iconAlignment, side: _lerpSides(a?.side, b?.side, t), shape: MaterialStateProperty.lerp(a?.shape, b?.shape, t, OutlinedBorder.lerp), mouseCursor: t < 0.5 ? a?.mouseCursor : b?.mouseCursor, diff --git a/packages/flutter/lib/src/material/button_style_button.dart b/packages/flutter/lib/src/material/button_style_button.dart index 3684e13dd2..a386d79a1e 100644 --- a/packages/flutter/lib/src/material/button_style_button.dart +++ b/packages/flutter/lib/src/material/button_style_button.dart @@ -86,7 +86,7 @@ abstract class ButtonStyleButton extends StatefulWidget { required this.clipBehavior, this.statesController, this.isSemanticButton = true, - this.iconAlignment = IconAlignment.start, + this.iconAlignment, this.tooltip, required this.child, }); @@ -158,7 +158,7 @@ abstract class ButtonStyleButton extends StatefulWidget { final bool? isSemanticButton; /// {@macro flutter.material.ButtonStyleButton.iconAlignment} - final IconAlignment iconAlignment; + final IconAlignment? iconAlignment; /// Text that describes the action that will occur when the button is pressed or /// hovered over. diff --git a/packages/flutter/lib/src/material/elevated_button.dart b/packages/flutter/lib/src/material/elevated_button.dart index 699e77035c..8ac7a5bb27 100644 --- a/packages/flutter/lib/src/material/elevated_button.dart +++ b/packages/flutter/lib/src/material/elevated_button.dart @@ -80,7 +80,6 @@ class ElevatedButton extends ButtonStyleButton { super.clipBehavior, super.statesController, required super.child, - super.iconAlignment, }); /// Create an elevated button from a pair of widgets that serve as the button's @@ -106,7 +105,7 @@ class ElevatedButton extends ButtonStyleButton { MaterialStatesController? statesController, Widget? icon, required Widget label, - IconAlignment iconAlignment = IconAlignment.start, + IconAlignment? iconAlignment, }) { if (icon == null) { return ElevatedButton( @@ -210,6 +209,7 @@ class ElevatedButton extends ButtonStyleButton { Color? surfaceTintColor, Color? iconColor, double? iconSize, + IconAlignment? iconAlignment, Color? disabledIconColor, Color? overlayColor, double? elevation, @@ -265,6 +265,7 @@ class ElevatedButton extends ButtonStyleButton { surfaceTintColor: ButtonStyleButton.allOrNull(surfaceTintColor), iconColor: ButtonStyleButton.defaultColor(iconColor, disabledIconColor), iconSize: ButtonStyleButton.allOrNull(iconSize), + iconAlignment: iconAlignment, elevation: elevationValue, padding: ButtonStyleButton.allOrNull(padding), minimumSize: ButtonStyleButton.allOrNull(minimumSize), @@ -478,7 +479,7 @@ class _ElevatedButtonWithIcon extends ElevatedButton { super.statesController, required Widget icon, required Widget label, - super.iconAlignment, + IconAlignment? iconAlignment, }) : super( autofocus: autofocus ?? false, child: _ElevatedButtonWithIconChild( @@ -525,16 +526,21 @@ class _ElevatedButtonWithIconChild extends StatelessWidget { final Widget label; final Widget icon; final ButtonStyle? buttonStyle; - final IconAlignment iconAlignment; + final IconAlignment? iconAlignment; @override Widget build(BuildContext context) { final double defaultFontSize = buttonStyle?.textStyle?.resolve(const {})?.fontSize ?? 14.0; final double scale = clampDouble(MediaQuery.textScalerOf(context).scale(defaultFontSize) / 14.0, 1.0, 2.0) - 1.0; final double gap = lerpDouble(8, 4, scale)!; + final ElevatedButtonThemeData elevatedButtonTheme = ElevatedButtonTheme.of(context); + final IconAlignment effectiveIconAlignment = iconAlignment + ?? elevatedButtonTheme.style?.iconAlignment + ?? buttonStyle?.iconAlignment + ?? IconAlignment.start; return Row( mainAxisSize: MainAxisSize.min, - children: iconAlignment == IconAlignment.start + children: effectiveIconAlignment == IconAlignment.start ? [icon, SizedBox(width: gap), Flexible(child: label)] : [Flexible(child: label), SizedBox(width: gap), icon], ); diff --git a/packages/flutter/lib/src/material/filled_button.dart b/packages/flutter/lib/src/material/filled_button.dart index bfb5bbce73..8a167ff564 100644 --- a/packages/flutter/lib/src/material/filled_button.dart +++ b/packages/flutter/lib/src/material/filled_button.dart @@ -82,7 +82,6 @@ class FilledButton extends ButtonStyleButton { super.clipBehavior = Clip.none, super.statesController, required super.child, - super.iconAlignment, }) : _variant = _FilledButtonVariant.filled; /// Create a filled button from [icon] and [label]. @@ -107,7 +106,7 @@ class FilledButton extends ButtonStyleButton { MaterialStatesController? statesController, Widget? icon, required Widget label, - IconAlignment iconAlignment = IconAlignment.start, + IconAlignment? iconAlignment, }) { if (icon == null) { return FilledButton( @@ -180,7 +179,7 @@ class FilledButton extends ButtonStyleButton { MaterialStatesController? statesController, Widget? icon, required Widget label, - IconAlignment iconAlignment = IconAlignment.start, + IconAlignment? iconAlignment, }) { if (icon == null) { return FilledButton.tonal( @@ -275,6 +274,7 @@ class FilledButton extends ButtonStyleButton { Color? surfaceTintColor, Color? iconColor, double? iconSize, + IconAlignment? iconAlignment, Color? disabledIconColor, Color? overlayColor, double? elevation, @@ -317,6 +317,7 @@ class FilledButton extends ButtonStyleButton { surfaceTintColor: ButtonStyleButton.allOrNull(surfaceTintColor), iconColor: ButtonStyleButton.defaultColor(iconColor, disabledIconColor), iconSize: ButtonStyleButton.allOrNull(iconSize), + iconAlignment: iconAlignment, elevation: ButtonStyleButton.allOrNull(elevation), padding: ButtonStyleButton.allOrNull(padding), minimumSize: ButtonStyleButton.allOrNull(minimumSize), @@ -501,7 +502,7 @@ class _FilledButtonWithIcon extends FilledButton { super.statesController, required Widget icon, required Widget label, - super.iconAlignment, + IconAlignment? iconAlignment, }) : super( autofocus: autofocus ?? false, child: _FilledButtonWithIconChild( @@ -525,7 +526,7 @@ class _FilledButtonWithIcon extends FilledButton { super.statesController, required Widget icon, required Widget label, - required IconAlignment iconAlignment, + IconAlignment? iconAlignment, }) : super.tonal( autofocus: autofocus ?? false, child: _FilledButtonWithIconChild( @@ -572,7 +573,7 @@ class _FilledButtonWithIconChild extends StatelessWidget { final Widget label; final Widget icon; final ButtonStyle? buttonStyle; - final IconAlignment iconAlignment; + final IconAlignment? iconAlignment; @override Widget build(BuildContext context) { @@ -581,9 +582,14 @@ class _FilledButtonWithIconChild extends StatelessWidget { // Adjust the gap based on the text scale factor. Start at 8, and lerp // to 4 based on how large the text is. final double gap = lerpDouble(8, 4, scale)!; + final FilledButtonThemeData filledButtonTheme = FilledButtonTheme.of(context); + final IconAlignment effectiveIconAlignment = iconAlignment + ?? filledButtonTheme.style?.iconAlignment + ?? buttonStyle?.iconAlignment + ?? IconAlignment.start; return Row( mainAxisSize: MainAxisSize.min, - children: iconAlignment == IconAlignment.start + children: effectiveIconAlignment == IconAlignment.start ? [icon, SizedBox(width: gap), Flexible(child: label)] : [Flexible(child: label), SizedBox(width: gap), icon], ); diff --git a/packages/flutter/lib/src/material/outlined_button.dart b/packages/flutter/lib/src/material/outlined_button.dart index d35af8c528..5cb79b2d83 100644 --- a/packages/flutter/lib/src/material/outlined_button.dart +++ b/packages/flutter/lib/src/material/outlined_button.dart @@ -84,7 +84,6 @@ class OutlinedButton extends ButtonStyleButton { super.clipBehavior, super.statesController, required super.child, - super.iconAlignment, }); /// Create a text button from a pair of widgets that serve as the button's @@ -110,7 +109,7 @@ class OutlinedButton extends ButtonStyleButton { MaterialStatesController? statesController, Widget? icon, required Widget label, - IconAlignment iconAlignment = IconAlignment.start, + IconAlignment? iconAlignment, }) { if (icon == null) { return OutlinedButton( @@ -197,6 +196,7 @@ class OutlinedButton extends ButtonStyleButton { Color? surfaceTintColor, Color? iconColor, double? iconSize, + IconAlignment? iconAlignment, Color? disabledIconColor, Color? overlayColor, double? elevation, @@ -243,6 +243,7 @@ class OutlinedButton extends ButtonStyleButton { surfaceTintColor: ButtonStyleButton.allOrNull(surfaceTintColor), iconColor: ButtonStyleButton.defaultColor(iconColor, disabledIconColor), iconSize: ButtonStyleButton.allOrNull(iconSize), + iconAlignment: iconAlignment, elevation: ButtonStyleButton.allOrNull(elevation), padding: ButtonStyleButton.allOrNull(padding), minimumSize: ButtonStyleButton.allOrNull(minimumSize), @@ -427,7 +428,7 @@ class _OutlinedButtonWithIcon extends OutlinedButton { super.statesController, required Widget icon, required Widget label, - super.iconAlignment, + IconAlignment? iconAlignment, }) : super( autofocus: autofocus ?? false, child: _OutlinedButtonWithIconChild( @@ -470,16 +471,21 @@ class _OutlinedButtonWithIconChild extends StatelessWidget { final Widget label; final Widget icon; final ButtonStyle? buttonStyle; - final IconAlignment iconAlignment; + final IconAlignment? iconAlignment; @override Widget build(BuildContext context) { final double defaultFontSize = buttonStyle?.textStyle?.resolve(const {})?.fontSize ?? 14.0; final double scale = clampDouble(MediaQuery.textScalerOf(context).scale(defaultFontSize) / 14.0, 1.0, 2.0) - 1.0; final double gap = lerpDouble(8, 4, scale)!; + final OutlinedButtonThemeData outlinedButtonTheme = OutlinedButtonTheme.of(context); + final IconAlignment effectiveIconAlignment = iconAlignment + ?? outlinedButtonTheme.style?.iconAlignment + ?? buttonStyle?.iconAlignment + ?? IconAlignment.start; return Row( mainAxisSize: MainAxisSize.min, - children: iconAlignment == IconAlignment.start + children: effectiveIconAlignment == IconAlignment.start ? [icon, SizedBox(width: gap), Flexible(child: label)] : [Flexible(child: label), SizedBox(width: gap), icon], ); diff --git a/packages/flutter/lib/src/material/text_button.dart b/packages/flutter/lib/src/material/text_button.dart index 39cb598443..2ff5ece118 100644 --- a/packages/flutter/lib/src/material/text_button.dart +++ b/packages/flutter/lib/src/material/text_button.dart @@ -93,7 +93,6 @@ class TextButton extends ButtonStyleButton { super.statesController, super.isSemanticButton, required Widget super.child, - super.iconAlignment, }); /// Create a text button from a pair of widgets that serve as the button's @@ -119,7 +118,7 @@ class TextButton extends ButtonStyleButton { MaterialStatesController? statesController, Widget? icon, required Widget label, - IconAlignment iconAlignment = IconAlignment.start, + IconAlignment? iconAlignment, }) { if (icon == null) { return TextButton( @@ -204,6 +203,7 @@ class TextButton extends ButtonStyleButton { Color? surfaceTintColor, Color? iconColor, double? iconSize, + IconAlignment? iconAlignment, Color? disabledIconColor, Color? overlayColor, double? elevation, @@ -254,6 +254,7 @@ class TextButton extends ButtonStyleButton { surfaceTintColor: ButtonStyleButton.allOrNull(surfaceTintColor), iconColor: iconColorProp, iconSize: ButtonStyleButton.allOrNull(iconSize), + iconAlignment: iconAlignment, elevation: ButtonStyleButton.allOrNull(elevation), padding: ButtonStyleButton.allOrNull(padding), minimumSize: ButtonStyleButton.allOrNull(minimumSize), @@ -456,7 +457,7 @@ class _TextButtonWithIcon extends TextButton { super.statesController, required Widget icon, required Widget label, - super.iconAlignment, + IconAlignment? iconAlignment, }) : super( autofocus: autofocus ?? false, child: _TextButtonWithIconChild( @@ -496,16 +497,21 @@ class _TextButtonWithIconChild extends StatelessWidget { final Widget label; final Widget icon; final ButtonStyle? buttonStyle; - final IconAlignment iconAlignment; + final IconAlignment? iconAlignment; @override Widget build(BuildContext context) { final double defaultFontSize = buttonStyle?.textStyle?.resolve(const {})?.fontSize ?? 14.0; final double scale = clampDouble(MediaQuery.textScalerOf(context).scale(defaultFontSize) / 14.0, 1.0, 2.0) - 1.0; final double gap = lerpDouble(8, 4, scale)!; + final TextButtonThemeData textButtonTheme = TextButtonTheme.of(context); + final IconAlignment effectiveIconAlignment = iconAlignment + ?? textButtonTheme.style?.iconAlignment + ?? buttonStyle?.iconAlignment + ?? IconAlignment.start; return Row( mainAxisSize: MainAxisSize.min, - children: iconAlignment == IconAlignment.start + children: effectiveIconAlignment == IconAlignment.start ? [icon, SizedBox(width: gap), Flexible(child: label)] : [Flexible(child: label), SizedBox(width: gap), icon], ); diff --git a/packages/flutter/test/material/elevated_button_test.dart b/packages/flutter/test/material/elevated_button_test.dart index 3523ca5d42..29484d5240 100644 --- a/packages/flutter/test/material/elevated_button_test.dart +++ b/packages/flutter/test/material/elevated_button_test.dart @@ -2295,7 +2295,7 @@ void main() { focusNode.dispose(); }); - testWidgets('Default iconAlignment', (WidgetTester tester) async { + testWidgets('Default ElevatedButton icon alignment', (WidgetTester tester) async { Widget buildWidget({ required TextDirection textDirection }) { return MaterialApp( home: Directionality( @@ -2330,7 +2330,7 @@ void main() { expect(buttonTopRight.dx, iconTopRight.dx + 16.0); // 16.0 - padding between icon and button edge. }); - testWidgets('iconAlignment can be customized', (WidgetTester tester) async { + testWidgets('ElevatedButton icon alignment can be customized', (WidgetTester tester) async { Widget buildWidget({ required TextDirection textDirection, required IconAlignment iconAlignment, @@ -2407,6 +2407,35 @@ void main() { expect(buttonTopLeft.dx, iconTopLeft.dx - 24.0); // 24.0 - padding between icon and button edge. }); + testWidgets('ElevatedButton icon alignment respects ButtonStyle.iconAlignment', (WidgetTester tester) async { + Widget buildButton({ IconAlignment? iconAlignment }) { + return MaterialApp( + home: Center( + child: ElevatedButton.icon( + style: ButtonStyle(iconAlignment: iconAlignment), + onPressed: () {}, + icon: const Icon(Icons.add), + label: const Text('button'), + ), + ), + ); + } + + await tester.pumpWidget(buildButton()); + + final Offset buttonTopLeft = tester.getTopLeft(find.byType(Material).last); + final Offset iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add)); + + expect(buttonTopLeft.dx, iconTopLeft.dx - 16.0); + + await tester.pumpWidget(buildButton(iconAlignment: IconAlignment.end)); + + final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last); + final Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add)); + + expect(buttonTopRight.dx, iconTopRight.dx + 24.0); + }); + // Regression test for https://github.com/flutter/flutter/issues/154798. testWidgets('ElevatedButton.styleFrom can customize the button icon', (WidgetTester tester) async { const Color iconColor = Color(0xFFF000FF); @@ -2415,15 +2444,18 @@ void main() { Widget buildButton({ bool enabled = true }) { return MaterialApp( home: Material( - child: ElevatedButton.icon( - style: ElevatedButton.styleFrom( - iconColor: iconColor, - iconSize: iconSize, - disabledIconColor: disabledIconColor, + child: Center( + child: ElevatedButton.icon( + style: ElevatedButton.styleFrom( + iconColor: iconColor, + iconSize: iconSize, + iconAlignment: IconAlignment.end, + disabledIconColor: disabledIconColor, + ), + onPressed: enabled ? () {} : null, + icon: const Icon(Icons.add), + label: const Text('Button'), ), - onPressed: enabled ? () {} : null, - icon: const Icon(Icons.add), - label: const Text('Button'), ), ), ); @@ -2437,5 +2469,9 @@ void main() { // Test disabled button. await tester.pumpWidget(buildButton(enabled: false)); expect(iconStyle(tester, Icons.add).color, disabledIconColor); + + final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last); + final Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add)); + expect(buttonTopRight.dx, iconTopRight.dx + 24.0); }); } diff --git a/packages/flutter/test/material/elevated_button_theme_test.dart b/packages/flutter/test/material/elevated_button_theme_test.dart index 4645c5040d..33486acf55 100644 --- a/packages/flutter/test/material/elevated_button_theme_test.dart +++ b/packages/flutter/test/material/elevated_button_theme_test.dart @@ -228,7 +228,7 @@ void main() { }); }); - testWidgets('Material 3: Theme shadowColor', (WidgetTester tester) async { + testWidgets('Material3 - ElevatedButton repsects Theme shadowColor', (WidgetTester tester) async { const ColorScheme colorScheme = ColorScheme.light(); const Color shadowColor = Color(0xff000001); const Color overriddenColor = Color(0xff000002); @@ -299,7 +299,7 @@ void main() { expect(material.shadowColor, shadowColor); }); - testWidgets('Material 2: Theme shadowColor', (WidgetTester tester) async { + testWidgets('Material2 - ElevatedButton repsects Theme shadowColor', (WidgetTester tester) async { const ColorScheme colorScheme = ColorScheme.light(); const Color shadowColor = Color(0xff000001); const Color overriddenColor = Color(0xff000002); @@ -368,4 +368,40 @@ void main() { material = tester.widget(buttonMaterialFinder); expect(material.shadowColor, shadowColor); }); + + testWidgets('ElevatedButton.icon respects ElevatedButtonTheme ButtonStyle.iconAlignment', (WidgetTester tester) async { + Widget buildButton({ IconAlignment? iconAlignment }) { + return MaterialApp( + theme: ThemeData( + elevatedButtonTheme: ElevatedButtonThemeData( + style: ButtonStyle(iconAlignment: iconAlignment), + ), + ), + home: Scaffold( + body: Center( + child: ElevatedButton.icon( + onPressed: () {}, + icon: const Icon(Icons.add), + label: const Text('button'), + ), + ), + ), + ); + } + + await tester.pumpWidget(buildButton()); + + final Offset buttonTopLeft = tester.getTopLeft(find.byType(Material).last); + final Offset iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add)); + + expect(buttonTopLeft.dx, iconTopLeft.dx - 16.0); + + await tester.pumpWidget(buildButton(iconAlignment: IconAlignment.end)); + await tester.pumpAndSettle(); + + final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last); + final Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add)); + + expect(buttonTopRight.dx, iconTopRight.dx + 24.0); + }); } diff --git a/packages/flutter/test/material/filled_button_test.dart b/packages/flutter/test/material/filled_button_test.dart index 6ce01c6941..d102c6a7a2 100644 --- a/packages/flutter/test/material/filled_button_test.dart +++ b/packages/flutter/test/material/filled_button_test.dart @@ -2408,7 +2408,7 @@ void main() { focusNode.dispose(); }); - testWidgets('Default iconAlignment', (WidgetTester tester) async { + testWidgets('Default FilledButton icon alignment', (WidgetTester tester) async { Widget buildWidget({ required TextDirection textDirection }) { return MaterialApp( home: Directionality( @@ -2443,7 +2443,7 @@ void main() { expect(buttonTopRight.dx, iconTopRight.dx + 16.0); // 16.0 - padding between icon and button edge. }); - testWidgets('iconAlignment can be customized', (WidgetTester tester) async { + testWidgets('FilledButton icon alignment can be customized', (WidgetTester tester) async { Widget buildWidget({ required TextDirection textDirection, required IconAlignment iconAlignment, @@ -2520,6 +2520,64 @@ void main() { expect(buttonTopLeft.dx, iconTopLeft.dx - 24.0); // 24.0 - padding between icon and button edge. }); + testWidgets('FilledButton icon alignment respects ButtonStyle.iconAlignment', (WidgetTester tester) async { + Widget buildButton({ IconAlignment? iconAlignment }) { + return MaterialApp( + home: Center( + child: FilledButton.icon( + style: ButtonStyle(iconAlignment: iconAlignment), + onPressed: () {}, + icon: const Icon(Icons.add), + label: const Text('button'), + ), + ), + ); + } + + await tester.pumpWidget(buildButton()); + + final Offset buttonTopLeft = tester.getTopLeft(find.byType(Material).last); + final Offset iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add)); + + expect(buttonTopLeft.dx, iconTopLeft.dx - 16.0); + + await tester.pumpWidget(buildButton(iconAlignment: IconAlignment.end)); + + final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last); + final Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add)); + + expect(buttonTopRight.dx, iconTopRight.dx + 24.0); + }); + + testWidgets('FilledButton tonal button icon alignment respects ButtonStyle.iconAlignment', (WidgetTester tester) async { + Widget buildButton({ IconAlignment? iconAlignment }) { + return MaterialApp( + home: Center( + child: FilledButton.tonalIcon( + style: ButtonStyle(iconAlignment: iconAlignment), + onPressed: () {}, + icon: const Icon(Icons.add), + label: const Text('button'), + ), + ), + ); + } + + await tester.pumpWidget(buildButton()); + + final Offset buttonTopLeft = tester.getTopLeft(find.byType(Material).last); + final Offset iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add)); + + expect(buttonTopLeft.dx, iconTopLeft.dx - 16.0); + + await tester.pumpWidget(buildButton(iconAlignment: IconAlignment.end)); + + final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last); + final Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add)); + + expect(buttonTopRight.dx, iconTopRight.dx + 24.0); + }); + testWidgets('Tonal icon default iconAlignment', (WidgetTester tester) async { Widget buildWidget({ required TextDirection textDirection }) { return MaterialApp( @@ -2645,6 +2703,7 @@ void main() { style: FilledButton.styleFrom( iconColor: iconColor, iconSize: iconSize, + iconAlignment: IconAlignment.end, disabledIconColor: disabledIconColor, ), onPressed: enabled ? () {} : null, @@ -2664,5 +2723,9 @@ void main() { // Test disabled button. await tester.pumpWidget(buildButton(enabled: false)); expect(iconStyle(tester, Icons.add).color, disabledIconColor); + + final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last); + final Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add)); + expect(buttonTopRight.dx, iconTopRight.dx + 24.0); }); } diff --git a/packages/flutter/test/material/filled_button_theme_test.dart b/packages/flutter/test/material/filled_button_theme_test.dart index e3a5fa5b57..c46298aae7 100644 --- a/packages/flutter/test/material/filled_button_theme_test.dart +++ b/packages/flutter/test/material/filled_button_theme_test.dart @@ -192,7 +192,7 @@ void main() { }); }); - testWidgets('Theme shadowColor', (WidgetTester tester) async { + testWidgets('FilledButton repsects Theme shadowColor', (WidgetTester tester) async { const ColorScheme colorScheme = ColorScheme.light(); const Color shadowColor = Color(0xff000001); const Color overriddenColor = Color(0xff000002); @@ -256,4 +256,76 @@ void main() { material = tester.widget(buttonMaterialFinder); expect(material.shadowColor, shadowColor); }); + + testWidgets('FilledButton.icon respects FilledButtonTheme ButtonStyle.iconAlignment', (WidgetTester tester) async { + Widget buildButton({ IconAlignment? iconAlignment }) { + return MaterialApp( + theme: ThemeData( + filledButtonTheme: FilledButtonThemeData( + style: ButtonStyle(iconAlignment: iconAlignment), + ), + ), + home: Scaffold( + body: Center( + child: FilledButton.icon( + onPressed: () {}, + icon: const Icon(Icons.add), + label: const Text('button'), + ), + ), + ), + ); + } + + await tester.pumpWidget(buildButton()); + + final Offset buttonTopLeft = tester.getTopLeft(find.byType(Material).last); + final Offset iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add)); + + expect(buttonTopLeft.dx, iconTopLeft.dx - 16.0); + + await tester.pumpWidget(buildButton(iconAlignment: IconAlignment.end)); + await tester.pumpAndSettle(); + + final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last); + final Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add)); + + expect(buttonTopRight.dx, iconTopRight.dx + 24.0); + }); + + testWidgets('Filled tonal button icon respects FilledButtonTheme ButtonStyle.iconAlignment', (WidgetTester tester) async { + Widget buildButton({ IconAlignment? iconAlignment }) { + return MaterialApp( + theme: ThemeData( + filledButtonTheme: FilledButtonThemeData( + style: ButtonStyle(iconAlignment: iconAlignment), + ), + ), + home: Scaffold( + body: Center( + child: FilledButton.tonalIcon( + onPressed: () {}, + icon: const Icon(Icons.add), + label: const Text('button'), + ), + ), + ), + ); + } + + await tester.pumpWidget(buildButton()); + + final Offset buttonTopLeft = tester.getTopLeft(find.byType(Material).last); + final Offset iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add)); + + expect(buttonTopLeft.dx, iconTopLeft.dx - 16.0); + + await tester.pumpWidget(buildButton(iconAlignment: IconAlignment.end)); + await tester.pumpAndSettle(); + + final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last); + final Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add)); + + expect(buttonTopRight.dx, iconTopRight.dx + 24.0); + }); } diff --git a/packages/flutter/test/material/outlined_button_test.dart b/packages/flutter/test/material/outlined_button_test.dart index 9fe25e976b..fcdec126d7 100644 --- a/packages/flutter/test/material/outlined_button_test.dart +++ b/packages/flutter/test/material/outlined_button_test.dart @@ -2471,7 +2471,7 @@ void main() { expect(material.color, backgroundColor); }); - testWidgets('Default iconAlignment', (WidgetTester tester) async { + testWidgets('Default OutlinedButton icon alignment', (WidgetTester tester) async { Widget buildWidget({ required TextDirection textDirection }) { return MaterialApp( home: Directionality( @@ -2506,7 +2506,7 @@ void main() { expect(buttonTopRight.dx, iconTopRight.dx + 16.0); // 16.0 - padding between icon and button edge. }); - testWidgets('iconAlignment can be customized', (WidgetTester tester) async { + testWidgets('OutlinedButton icon alignment can be customized', (WidgetTester tester) async { Widget buildWidget({ required TextDirection textDirection, required IconAlignment iconAlignment, @@ -2583,6 +2583,35 @@ void main() { expect(buttonTopLeft.dx, iconTopLeft.dx - 24.0); // 24.0 - padding between icon and button edge. }); + testWidgets('OutlinedButton icon alignment respects ButtonStyle.iconAlignment', (WidgetTester tester) async { + Widget buildButton({ IconAlignment? iconAlignment }) { + return MaterialApp( + home: Center( + child: OutlinedButton.icon( + style: ButtonStyle(iconAlignment: iconAlignment), + onPressed: () {}, + icon: const Icon(Icons.add), + label: const Text('button'), + ), + ), + ); + } + + await tester.pumpWidget(buildButton()); + + final Offset buttonTopLeft = tester.getTopLeft(find.byType(Material).last); + final Offset iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add)); + + expect(buttonTopLeft.dx, iconTopLeft.dx - 16.0); + + await tester.pumpWidget(buildButton(iconAlignment: IconAlignment.end)); + + final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last); + final Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add)); + + expect(buttonTopRight.dx, iconTopRight.dx + 24.0); + }); + testWidgets("OutlinedButton.icon response doesn't hover when disabled", (WidgetTester tester) async { FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTouch; final FocusNode focusNode = FocusNode(debugLabel: 'OutlinedButton.icon Focus'); @@ -2763,6 +2792,7 @@ void main() { style: OutlinedButton.styleFrom( iconColor: iconColor, iconSize: iconSize, + iconAlignment: IconAlignment.end, disabledIconColor: disabledIconColor, ), onPressed: enabled ? () {} : null, @@ -2782,5 +2812,9 @@ void main() { // Test disabled button. await tester.pumpWidget(buildButton(enabled: false)); expect(iconStyle(tester, Icons.add).color, disabledIconColor); + + final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last); + final Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add)); + expect(buttonTopRight.dx, iconTopRight.dx + 24.0); }); } diff --git a/packages/flutter/test/material/outlined_button_theme_test.dart b/packages/flutter/test/material/outlined_button_theme_test.dart index 3d71ca4cee..3d3b426d70 100644 --- a/packages/flutter/test/material/outlined_button_theme_test.dart +++ b/packages/flutter/test/material/outlined_button_theme_test.dart @@ -235,7 +235,7 @@ void main() { }); }); - testWidgets('Material3: Theme shadowColor', (WidgetTester tester) async { + testWidgets('Material3 - OutlinedButton repsects Theme shadowColor', (WidgetTester tester) async { const ColorScheme colorScheme = ColorScheme.light(); const Color shadowColor = Color(0xff000001); const Color overriddenColor = Color(0xff000002); @@ -306,7 +306,7 @@ void main() { expect(material.shadowColor, shadowColor); }); - testWidgets('Material2: Theme shadowColor', (WidgetTester tester) async { + testWidgets('Material2 - OutlinedButton repsects Theme shadowColor', (WidgetTester tester) async { const ColorScheme colorScheme = ColorScheme.light(); const Color shadowColor = Color(0xff000001); const Color overriddenColor = Color(0xff000002); @@ -375,4 +375,40 @@ void main() { material = tester.widget(buttonMaterialFinder); expect(material.shadowColor, shadowColor); }); + + testWidgets('OutlinedButton.icon alignment respects OutlinedButtonTheme ButtonStyle.iconAlignment', (WidgetTester tester) async { + Widget buildButton({ IconAlignment? iconAlignment }) { + return MaterialApp( + theme: ThemeData( + outlinedButtonTheme: OutlinedButtonThemeData( + style: ButtonStyle(iconAlignment: iconAlignment), + ), + ), + home: Scaffold( + body: Center( + child: OutlinedButton.icon( + onPressed: () {}, + icon: const Icon(Icons.add), + label: const Text('button'), + ), + ), + ), + ); + } + + await tester.pumpWidget(buildButton()); + + final Offset buttonTopLeft = tester.getTopLeft(find.byType(Material).last); + final Offset iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add)); + + expect(buttonTopLeft.dx, iconTopLeft.dx - 16.0); + + await tester.pumpWidget(buildButton(iconAlignment: IconAlignment.end)); + await tester.pumpAndSettle(); + + final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last); + final Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add)); + + expect(buttonTopRight.dx, iconTopRight.dx + 24.0); + }); } diff --git a/packages/flutter/test/material/text_button_test.dart b/packages/flutter/test/material/text_button_test.dart index 158be30081..75344ce253 100644 --- a/packages/flutter/test/material/text_button_test.dart +++ b/packages/flutter/test/material/text_button_test.dart @@ -2304,7 +2304,7 @@ void main() { expect(material.color, backgroundColor); }); - testWidgets('Default iconAlignment', (WidgetTester tester) async { + testWidgets('Default TextButton icon alignment', (WidgetTester tester) async { Widget buildWidget({ required TextDirection textDirection }) { return MaterialApp( home: Directionality( @@ -2339,7 +2339,7 @@ void main() { expect(buttonTopRight.dx, iconTopRight.dx + 12.0); // 12.0 - padding between icon and button edge. }); - testWidgets('iconAlignment can be customized', (WidgetTester tester) async { + testWidgets('TextButton icon alignment can be customized', (WidgetTester tester) async { Widget buildWidget({ required TextDirection textDirection, required IconAlignment iconAlignment, @@ -2416,6 +2416,35 @@ void main() { expect(buttonTopLeft.dx, iconTopLeft.dx - 16.0); // 16.0 - padding between icon and button edge. }); + testWidgets('TextButton icon alignment respects ButtonStyle.iconAlignment', (WidgetTester tester) async { + Widget buildButton({ IconAlignment? iconAlignment }) { + return MaterialApp( + home: Center( + child: TextButton.icon( + style: ButtonStyle(iconAlignment: iconAlignment), + onPressed: () {}, + icon: const Icon(Icons.add), + label: const Text('button'), + ), + ), + ); + } + + await tester.pumpWidget(buildButton()); + + final Offset buttonTopLeft = tester.getTopLeft(find.byType(Material).last); + final Offset iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add)); + + expect(buttonTopLeft.dx, iconTopLeft.dx - 12.0); + + await tester.pumpWidget(buildButton(iconAlignment: IconAlignment.end)); + + final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last); + final Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add)); + + expect(buttonTopRight.dx, iconTopRight.dx + 16.0); + }); + testWidgets('treats a hovering stylus like a mouse', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); addTearDown(focusNode.dispose); @@ -2476,6 +2505,7 @@ void main() { style: TextButton.styleFrom( iconColor: iconColor, iconSize: iconSize, + iconAlignment: IconAlignment.end, disabledIconColor: disabledIconColor, ), onPressed: enabled ? () {} : null, @@ -2495,5 +2525,9 @@ void main() { // Test disabled button. await tester.pumpWidget(buildButton(enabled: false)); expect(iconStyle(tester, Icons.add).color, disabledIconColor); + + final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last); + final Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add)); + expect(buttonTopRight.dx, iconTopRight.dx + 16.0); }); } diff --git a/packages/flutter/test/material/text_button_theme_test.dart b/packages/flutter/test/material/text_button_theme_test.dart index 298febae52..407d3c2202 100644 --- a/packages/flutter/test/material/text_button_theme_test.dart +++ b/packages/flutter/test/material/text_button_theme_test.dart @@ -239,7 +239,7 @@ void main() { }); }); - testWidgets('Material3: Theme shadowColor', (WidgetTester tester) async { + testWidgets('Material3 - TextButton repsects Theme shadowColor', (WidgetTester tester) async { const ColorScheme colorScheme = ColorScheme.light(); const Color shadowColor = Color(0xff000001); const Color overriddenColor = Color(0xff000002); @@ -310,7 +310,7 @@ void main() { expect(material.shadowColor, shadowColor); }); - testWidgets('Material2: Theme shadowColor', (WidgetTester tester) async { + testWidgets('Material2 - TextButton repsects Theme shadowColor', (WidgetTester tester) async { const ColorScheme colorScheme = ColorScheme.light(); const Color shadowColor = Color(0xff000001); const Color overriddenColor = Color(0xff000002); @@ -379,4 +379,40 @@ void main() { material = tester.widget(buttonMaterialFinder); expect(material.shadowColor, shadowColor); }); + + testWidgets('TextButton.icon respects TextButtonTheme ButtonStyle.iconAlignment', (WidgetTester tester) async { + Widget buildButton({ IconAlignment? iconAlignment }) { + return MaterialApp( + theme: ThemeData( + textButtonTheme: TextButtonThemeData( + style: ButtonStyle(iconAlignment: iconAlignment), + ), + ), + home: Scaffold( + body: Center( + child: TextButton.icon( + onPressed: () {}, + icon: const Icon(Icons.add), + label: const Text('button'), + ), + ), + ), + ); + } + + await tester.pumpWidget(buildButton()); + + final Offset buttonTopLeft = tester.getTopLeft(find.byType(Material).last); + final Offset iconTopLeft = tester.getTopLeft(find.byIcon(Icons.add)); + + expect(buttonTopLeft.dx, iconTopLeft.dx - 12.0); + + await tester.pumpWidget(buildButton(iconAlignment: IconAlignment.end)); + await tester.pumpAndSettle(); + + final Offset buttonTopRight = tester.getTopRight(find.byType(Material).last); + final Offset iconTopRight = tester.getTopRight(find.byIcon(Icons.add)); + + expect(buttonTopRight.dx, iconTopRight.dx + 16.0); + }); }