Unverified Commit 2652b9a3 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Convert button `.icon` and `.tonalIcon` constructors to take nullable icons. (#142644)

## Description

This changes the factory constructors for `TextButton.icon`, `ElevatedButton.icon`, `FilledButton.icon`, and `FilledButton.tonalIcon` to take nullable icons. If the icon is null, then the "regular" version of the button is created.

## Tests
 - Added tests for all four constructors.
parent b34ee073
...@@ -81,6 +81,8 @@ class ElevatedButton extends ButtonStyleButton { ...@@ -81,6 +81,8 @@ class ElevatedButton extends ButtonStyleButton {
/// ///
/// The icon and label are arranged in a row and padded by 12 logical pixels /// 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. /// 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({ factory ElevatedButton.icon({
Key? key, Key? key,
required VoidCallback? onPressed, required VoidCallback? onPressed,
...@@ -92,9 +94,39 @@ class ElevatedButton extends ButtonStyleButton { ...@@ -92,9 +94,39 @@ class ElevatedButton extends ButtonStyleButton {
bool? autofocus, bool? autofocus,
Clip? clipBehavior, Clip? clipBehavior,
MaterialStatesController? statesController, MaterialStatesController? statesController,
required Widget icon, Widget? icon,
required Widget label, 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 /// A static convenience method that constructs an elevated button
/// [ButtonStyle] given simple values. /// [ButtonStyle] given simple values.
......
...@@ -81,6 +81,8 @@ class FilledButton extends ButtonStyleButton { ...@@ -81,6 +81,8 @@ class FilledButton extends ButtonStyleButton {
/// ///
/// The icon and label are arranged in a row with padding at the start and end /// The icon and label are arranged in a row with padding at the start and end
/// and a gap between them. /// and a gap between them.
///
/// If [icon] is null, will create a [FilledButton] instead.
factory FilledButton.icon({ factory FilledButton.icon({
Key? key, Key? key,
required VoidCallback? onPressed, required VoidCallback? onPressed,
...@@ -92,9 +94,39 @@ class FilledButton extends ButtonStyleButton { ...@@ -92,9 +94,39 @@ class FilledButton extends ButtonStyleButton {
bool? autofocus, bool? autofocus,
Clip? clipBehavior, Clip? clipBehavior,
MaterialStatesController? statesController, MaterialStatesController? statesController,
required Widget icon, Widget? icon,
required Widget label, 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. /// Create a tonal variant of FilledButton.
/// ///
...@@ -118,8 +150,10 @@ class FilledButton extends ButtonStyleButton { ...@@ -118,8 +150,10 @@ class FilledButton extends ButtonStyleButton {
/// Create a filled tonal button from [icon] and [label]. /// 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 /// The [icon] and [label] are arranged in a row with padding at the start and
/// and a gap between them. /// end and a gap between them.
///
/// If [icon] is null, will create a [FilledButton.tonal] instead.
factory FilledButton.tonalIcon({ factory FilledButton.tonalIcon({
Key? key, Key? key,
required VoidCallback? onPressed, required VoidCallback? onPressed,
...@@ -131,9 +165,24 @@ class FilledButton extends ButtonStyleButton { ...@@ -131,9 +165,24 @@ class FilledButton extends ButtonStyleButton {
bool? autofocus, bool? autofocus,
Clip? clipBehavior, Clip? clipBehavior,
MaterialStatesController? statesController, MaterialStatesController? statesController,
required Widget icon, Widget? icon,
required Widget label, 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( return _FilledButtonWithIcon.tonal(
key: key, key: key,
onPressed: onPressed, onPressed: onPressed,
......
...@@ -85,7 +85,9 @@ class OutlinedButton extends ButtonStyleButton { ...@@ -85,7 +85,9 @@ class OutlinedButton extends ButtonStyleButton {
/// ///
/// The icon and label are arranged in a row and padded by 12 logical pixels /// 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. /// 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, Key? key,
required VoidCallback? onPressed, required VoidCallback? onPressed,
VoidCallback? onLongPress, VoidCallback? onLongPress,
...@@ -94,9 +96,35 @@ class OutlinedButton extends ButtonStyleButton { ...@@ -94,9 +96,35 @@ class OutlinedButton extends ButtonStyleButton {
bool? autofocus, bool? autofocus,
Clip? clipBehavior, Clip? clipBehavior,
MaterialStatesController? statesController, MaterialStatesController? statesController,
required Widget icon, Widget? icon,
required Widget label, 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 /// A static convenience method that constructs an outlined button
/// [ButtonStyle] given simple values. /// [ButtonStyle] given simple values.
......
...@@ -105,9 +105,38 @@ class TextButton extends ButtonStyleButton { ...@@ -105,9 +105,38 @@ class TextButton extends ButtonStyleButton {
bool? autofocus, bool? autofocus,
Clip? clipBehavior, Clip? clipBehavior,
MaterialStatesController? statesController, MaterialStatesController? statesController,
required Widget icon, Widget? icon,
required Widget label, 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 /// A static convenience method that constructs a text button
/// [ButtonStyle] given simple values. /// [ButtonStyle] given simple values.
......
...@@ -159,6 +159,45 @@ void main() { ...@@ -159,6 +159,45 @@ void main() {
expect(material.type, MaterialType.button); 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 { testWidgets('Default ElevatedButton meets a11y contrast guidelines', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
......
...@@ -127,6 +127,84 @@ void main() { ...@@ -127,6 +127,84 @@ void main() {
await tester.pumpAndSettle(); 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 { testWidgets('FilledButton.tonal, FilledButton.tonalIcon defaults', (WidgetTester tester) async {
const ColorScheme colorScheme = ColorScheme.light(); const ColorScheme colorScheme = ColorScheme.light();
final ThemeData theme = ThemeData.from(colorScheme: colorScheme); final ThemeData theme = ThemeData.from(colorScheme: colorScheme);
......
...@@ -174,6 +174,45 @@ void main() { ...@@ -174,6 +174,45 @@ void main() {
expect(material.type, MaterialType.button); 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 { testWidgets('OutlinedButton default overlayColor resolves pressed state', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
final ThemeData theme = ThemeData(useMaterial3: true); final ThemeData theme = ThemeData(useMaterial3: true);
......
...@@ -154,6 +154,45 @@ void main() { ...@@ -154,6 +154,45 @@ void main() {
expect(material.type, MaterialType.button); 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 { testWidgets('Default TextButton meets a11y contrast guidelines', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment