diff --git a/packages/flutter/lib/src/material/elevated_button.dart b/packages/flutter/lib/src/material/elevated_button.dart index ae4828f687..802a482ea7 100644 --- a/packages/flutter/lib/src/material/elevated_button.dart +++ b/packages/flutter/lib/src/material/elevated_button.dart @@ -81,6 +81,8 @@ class ElevatedButton extends ButtonStyleButton { /// /// The icon and label are arranged in a row and padded by 12 logical pixels /// at the start, and 16 at the end, with an 8 pixel gap in between. + /// + /// If [icon] is null, will create an [ElevatedButton] instead. factory ElevatedButton.icon({ Key? key, required VoidCallback? onPressed, @@ -92,9 +94,39 @@ class ElevatedButton extends ButtonStyleButton { bool? autofocus, Clip? clipBehavior, MaterialStatesController? statesController, - required Widget icon, + Widget? icon, required Widget label, - }) = _ElevatedButtonWithIcon; + }) { + if (icon == null) { + return ElevatedButton( + key: key, + onPressed: onPressed, + onLongPress: onLongPress, + onHover: onHover, + onFocusChange: onFocusChange, + style: style, + focusNode: focusNode, + autofocus: autofocus ?? false, + clipBehavior: clipBehavior ?? Clip.none, + statesController: statesController, + child: label, + ); + } + return _ElevatedButtonWithIcon( + key: key, + onPressed: onPressed, + onLongPress: onLongPress, + onHover: onHover, + onFocusChange: onFocusChange, + style: style, + focusNode: focusNode, + autofocus: autofocus ?? false, + clipBehavior: clipBehavior ?? Clip.none, + statesController: statesController, + icon: icon, + label: label, + ); + } /// A static convenience method that constructs an elevated button /// [ButtonStyle] given simple values. diff --git a/packages/flutter/lib/src/material/filled_button.dart b/packages/flutter/lib/src/material/filled_button.dart index 651f9d42d8..d2a9c6cfd4 100644 --- a/packages/flutter/lib/src/material/filled_button.dart +++ b/packages/flutter/lib/src/material/filled_button.dart @@ -81,6 +81,8 @@ class FilledButton extends ButtonStyleButton { /// /// The icon and label are arranged in a row with padding at the start and end /// and a gap between them. + /// + /// If [icon] is null, will create a [FilledButton] instead. factory FilledButton.icon({ Key? key, required VoidCallback? onPressed, @@ -92,9 +94,39 @@ class FilledButton extends ButtonStyleButton { bool? autofocus, Clip? clipBehavior, MaterialStatesController? statesController, - required Widget icon, + Widget? icon, required Widget label, - }) = _FilledButtonWithIcon; + }) { + if (icon == null) { + return FilledButton( + key: key, + onPressed: onPressed, + onLongPress: onLongPress, + onHover: onHover, + onFocusChange: onFocusChange, + style: style, + focusNode: focusNode, + autofocus: autofocus ?? false, + clipBehavior: clipBehavior ?? Clip.none, + statesController: statesController, + child: label, + ); + } + return _FilledButtonWithIcon( + key: key, + onPressed: onPressed, + onLongPress: onLongPress, + onHover: onHover, + onFocusChange: onFocusChange, + style: style, + focusNode: focusNode, + autofocus: autofocus ?? false, + clipBehavior: clipBehavior ?? Clip.none, + statesController: statesController, + icon: icon, + label: label, + ); + } /// Create a tonal variant of FilledButton. /// @@ -118,8 +150,10 @@ class FilledButton extends ButtonStyleButton { /// Create a filled tonal button from [icon] and [label]. /// - /// The icon and label are arranged in a row with padding at the start and end - /// and a gap between them. + /// The [icon] and [label] are arranged in a row with padding at the start and + /// end and a gap between them. + /// + /// If [icon] is null, will create a [FilledButton.tonal] instead. factory FilledButton.tonalIcon({ Key? key, required VoidCallback? onPressed, @@ -131,9 +165,24 @@ class FilledButton extends ButtonStyleButton { bool? autofocus, Clip? clipBehavior, MaterialStatesController? statesController, - required Widget icon, + Widget? icon, required Widget label, }) { + if (icon == null) { + return FilledButton.tonal( + key: key, + onPressed: onPressed, + onLongPress: onLongPress, + onHover: onHover, + onFocusChange: onFocusChange, + style: style, + focusNode: focusNode, + autofocus: autofocus ?? false, + clipBehavior: clipBehavior ?? Clip.none, + statesController: statesController, + child: label, + ); + } return _FilledButtonWithIcon.tonal( key: key, onPressed: onPressed, diff --git a/packages/flutter/lib/src/material/outlined_button.dart b/packages/flutter/lib/src/material/outlined_button.dart index 2f805f8d70..abba6a7e05 100644 --- a/packages/flutter/lib/src/material/outlined_button.dart +++ b/packages/flutter/lib/src/material/outlined_button.dart @@ -85,7 +85,9 @@ class OutlinedButton extends ButtonStyleButton { /// /// The icon and label are arranged in a row and padded by 12 logical pixels /// at the start, and 16 at the end, with an 8 pixel gap in between. - factory OutlinedButton.icon({ + /// + /// If [icon] is null, will create an [OutlinedButton] instead. + factory OutlinedButton.icon({ Key? key, required VoidCallback? onPressed, VoidCallback? onLongPress, @@ -94,9 +96,35 @@ class OutlinedButton extends ButtonStyleButton { bool? autofocus, Clip? clipBehavior, MaterialStatesController? statesController, - required Widget icon, + Widget? icon, required Widget label, - }) = _OutlinedButtonWithIcon; + }) { + if (icon == null) { + return OutlinedButton( + key: key, + onPressed: onPressed, + onLongPress: onLongPress, + style: style, + focusNode: focusNode, + autofocus: autofocus ?? false, + clipBehavior: clipBehavior ?? Clip.none, + statesController: statesController, + child: label, + ); + } + return _OutlinedButtonWithIcon( + key: key, + onPressed: onPressed, + onLongPress: onLongPress, + style: style, + focusNode: focusNode, + autofocus: autofocus ?? false, + clipBehavior: clipBehavior ?? Clip.none, + statesController: statesController, + icon: icon, + label: label, + ); + } /// A static convenience method that constructs an outlined button /// [ButtonStyle] given simple values. diff --git a/packages/flutter/lib/src/material/text_button.dart b/packages/flutter/lib/src/material/text_button.dart index 229ca51335..325e1d3312 100644 --- a/packages/flutter/lib/src/material/text_button.dart +++ b/packages/flutter/lib/src/material/text_button.dart @@ -105,9 +105,38 @@ class TextButton extends ButtonStyleButton { bool? autofocus, Clip? clipBehavior, MaterialStatesController? statesController, - required Widget icon, + Widget? icon, required Widget label, - }) = _TextButtonWithIcon; + }) { + if (icon == null) { + return TextButton( + key: key, + onPressed: onPressed, + onLongPress: onLongPress, + onHover: onHover, + onFocusChange: onFocusChange, + style: style, + focusNode: focusNode, + autofocus: autofocus ?? false, + clipBehavior: clipBehavior ?? Clip.none, + statesController: statesController, + child: label, + ); + } + return _TextButtonWithIcon( key: key, + onPressed: onPressed, + onLongPress: onLongPress, + onHover: onHover, + onFocusChange: onFocusChange, + style: style, + focusNode: focusNode, + autofocus: autofocus ?? false, + clipBehavior: clipBehavior ?? Clip.none, + statesController: statesController, + icon: icon, + label: label, + ); + } /// A static convenience method that constructs a text button /// [ButtonStyle] given simple values. diff --git a/packages/flutter/test/material/elevated_button_test.dart b/packages/flutter/test/material/elevated_button_test.dart index 44e3e1d50e..e7f1a6e68c 100644 --- a/packages/flutter/test/material/elevated_button_test.dart +++ b/packages/flutter/test/material/elevated_button_test.dart @@ -159,6 +159,45 @@ void main() { expect(material.type, MaterialType.button); }); + 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); + final Key iconButtonKey = UniqueKey(); + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: Center( + child: ElevatedButton.icon( + key: iconButtonKey, + onPressed: () { }, + icon: const Icon(Icons.add), + label: const Text('label'), + ), + ), + ), + ); + + expect(find.byIcon(Icons.add), findsOneWidget); + expect(find.text('label'), findsOneWidget); + + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: Center( + child: ElevatedButton.icon( + key: iconButtonKey, + onPressed: () { }, + // No icon specified. + label: const Text('label'), + ), + ), + ), + ); + + expect(find.byIcon(Icons.add), findsNothing); + expect(find.text('label'), findsOneWidget); + }); + testWidgets('Default ElevatedButton meets a11y contrast guidelines', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); diff --git a/packages/flutter/test/material/filled_button_test.dart b/packages/flutter/test/material/filled_button_test.dart index 19b69e0d4e..535d8ba52f 100644 --- a/packages/flutter/test/material/filled_button_test.dart +++ b/packages/flutter/test/material/filled_button_test.dart @@ -127,6 +127,84 @@ void main() { await tester.pumpAndSettle(); }); + 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); + final Key iconButtonKey = UniqueKey(); + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: Center( + child: FilledButton.icon( + key: iconButtonKey, + onPressed: () { }, + icon: const Icon(Icons.add), + label: const Text('label'), + ), + ), + ), + ); + + expect(find.byIcon(Icons.add), findsOneWidget); + expect(find.text('label'), findsOneWidget); + + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: Center( + child: FilledButton.icon( + key: iconButtonKey, + onPressed: () { }, + // No icon specified. + label: const Text('label'), + ), + ), + ), + ); + + expect(find.byIcon(Icons.add), findsNothing); + expect(find.text('label'), findsOneWidget); + }); + + testWidgets('FilledButton.tonalIcon produces the correct widgets if icon is null', (WidgetTester tester) async { + const ColorScheme colorScheme = ColorScheme.light(); + final ThemeData theme = ThemeData.from(colorScheme: colorScheme); + final Key iconButtonKey = UniqueKey(); + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: Center( + child: FilledButton.tonalIcon( + key: iconButtonKey, + onPressed: () { }, + icon: const Icon(Icons.add), + label: const Text('label'), + ), + ), + ), + ); + + expect(find.byIcon(Icons.add), findsOneWidget); + expect(find.text('label'), findsOneWidget); + + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: Center( + child: FilledButton.tonalIcon( + key: iconButtonKey, + onPressed: () { }, + // No icon specified. + label: const Text('label'), + ), + ), + ), + ); + + expect(find.byIcon(Icons.add), findsNothing); + expect(find.text('label'), findsOneWidget); + }); + testWidgets('FilledButton.tonal, FilledButton.tonalIcon defaults', (WidgetTester tester) async { const ColorScheme colorScheme = ColorScheme.light(); final ThemeData theme = ThemeData.from(colorScheme: colorScheme); diff --git a/packages/flutter/test/material/outlined_button_test.dart b/packages/flutter/test/material/outlined_button_test.dart index 301a0d3aa7..eec8653181 100644 --- a/packages/flutter/test/material/outlined_button_test.dart +++ b/packages/flutter/test/material/outlined_button_test.dart @@ -174,6 +174,45 @@ void main() { expect(material.type, MaterialType.button); }); + 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); + final Key iconButtonKey = UniqueKey(); + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: Center( + child: OutlinedButton.icon( + key: iconButtonKey, + onPressed: () { }, + icon: const Icon(Icons.add), + label: const Text('label'), + ), + ), + ), + ); + + expect(find.byIcon(Icons.add), findsOneWidget); + expect(find.text('label'), findsOneWidget); + + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: Center( + child: OutlinedButton.icon( + key: iconButtonKey, + onPressed: () { }, + // No icon specified. + label: const Text('label'), + ), + ), + ), + ); + + expect(find.byIcon(Icons.add), findsNothing); + expect(find.text('label'), findsOneWidget); + }); + testWidgets('OutlinedButton default overlayColor resolves pressed state', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); final ThemeData theme = ThemeData(useMaterial3: true); diff --git a/packages/flutter/test/material/text_button_test.dart b/packages/flutter/test/material/text_button_test.dart index f197f68d2c..fc86ae3aef 100644 --- a/packages/flutter/test/material/text_button_test.dart +++ b/packages/flutter/test/material/text_button_test.dart @@ -154,6 +154,45 @@ void main() { expect(material.type, MaterialType.button); }); + 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); + final Key iconButtonKey = UniqueKey(); + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: Center( + child: TextButton.icon( + key: iconButtonKey, + onPressed: () { }, + icon: const Icon(Icons.add), + label: const Text('label'), + ), + ), + ), + ); + + expect(find.byIcon(Icons.add), findsOneWidget); + expect(find.text('label'), findsOneWidget); + + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: Center( + child: TextButton.icon( + key: iconButtonKey, + onPressed: () { }, + // No icon specified. + label: const Text('label'), + ), + ), + ), + ); + + expect(find.byIcon(Icons.add), findsNothing); + expect(find.text('label'), findsOneWidget); + }); + testWidgets('Default TextButton meets a11y contrast guidelines', (WidgetTester tester) async { final FocusNode focusNode = FocusNode();