Skip to content

Commit 2652b9a

Browse files
authored
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.
1 parent b34ee07 commit 2652b9a

File tree

8 files changed

+345
-12
lines changed

8 files changed

+345
-12
lines changed

packages/flutter/lib/src/material/elevated_button.dart

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ class ElevatedButton extends ButtonStyleButton {
8181
///
8282
/// The icon and label are arranged in a row and padded by 12 logical pixels
8383
/// at the start, and 16 at the end, with an 8 pixel gap in between.
84+
///
85+
/// If [icon] is null, will create an [ElevatedButton] instead.
8486
factory ElevatedButton.icon({
8587
Key? key,
8688
required VoidCallback? onPressed,
@@ -92,9 +94,39 @@ class ElevatedButton extends ButtonStyleButton {
9294
bool? autofocus,
9395
Clip? clipBehavior,
9496
MaterialStatesController? statesController,
95-
required Widget icon,
97+
Widget? icon,
9698
required Widget label,
97-
}) = _ElevatedButtonWithIcon;
99+
}) {
100+
if (icon == null) {
101+
return ElevatedButton(
102+
key: key,
103+
onPressed: onPressed,
104+
onLongPress: onLongPress,
105+
onHover: onHover,
106+
onFocusChange: onFocusChange,
107+
style: style,
108+
focusNode: focusNode,
109+
autofocus: autofocus ?? false,
110+
clipBehavior: clipBehavior ?? Clip.none,
111+
statesController: statesController,
112+
child: label,
113+
);
114+
}
115+
return _ElevatedButtonWithIcon(
116+
key: key,
117+
onPressed: onPressed,
118+
onLongPress: onLongPress,
119+
onHover: onHover,
120+
onFocusChange: onFocusChange,
121+
style: style,
122+
focusNode: focusNode,
123+
autofocus: autofocus ?? false,
124+
clipBehavior: clipBehavior ?? Clip.none,
125+
statesController: statesController,
126+
icon: icon,
127+
label: label,
128+
);
129+
}
98130

99131
/// A static convenience method that constructs an elevated button
100132
/// [ButtonStyle] given simple values.

packages/flutter/lib/src/material/filled_button.dart

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ class FilledButton extends ButtonStyleButton {
8181
///
8282
/// The icon and label are arranged in a row with padding at the start and end
8383
/// and a gap between them.
84+
///
85+
/// If [icon] is null, will create a [FilledButton] instead.
8486
factory FilledButton.icon({
8587
Key? key,
8688
required VoidCallback? onPressed,
@@ -92,9 +94,39 @@ class FilledButton extends ButtonStyleButton {
9294
bool? autofocus,
9395
Clip? clipBehavior,
9496
MaterialStatesController? statesController,
95-
required Widget icon,
97+
Widget? icon,
9698
required Widget label,
97-
}) = _FilledButtonWithIcon;
99+
}) {
100+
if (icon == null) {
101+
return FilledButton(
102+
key: key,
103+
onPressed: onPressed,
104+
onLongPress: onLongPress,
105+
onHover: onHover,
106+
onFocusChange: onFocusChange,
107+
style: style,
108+
focusNode: focusNode,
109+
autofocus: autofocus ?? false,
110+
clipBehavior: clipBehavior ?? Clip.none,
111+
statesController: statesController,
112+
child: label,
113+
);
114+
}
115+
return _FilledButtonWithIcon(
116+
key: key,
117+
onPressed: onPressed,
118+
onLongPress: onLongPress,
119+
onHover: onHover,
120+
onFocusChange: onFocusChange,
121+
style: style,
122+
focusNode: focusNode,
123+
autofocus: autofocus ?? false,
124+
clipBehavior: clipBehavior ?? Clip.none,
125+
statesController: statesController,
126+
icon: icon,
127+
label: label,
128+
);
129+
}
98130

99131
/// Create a tonal variant of FilledButton.
100132
///
@@ -118,8 +150,10 @@ class FilledButton extends ButtonStyleButton {
118150

119151
/// Create a filled tonal button from [icon] and [label].
120152
///
121-
/// The icon and label are arranged in a row with padding at the start and end
122-
/// and a gap between them.
153+
/// The [icon] and [label] are arranged in a row with padding at the start and
154+
/// end and a gap between them.
155+
///
156+
/// If [icon] is null, will create a [FilledButton.tonal] instead.
123157
factory FilledButton.tonalIcon({
124158
Key? key,
125159
required VoidCallback? onPressed,
@@ -131,9 +165,24 @@ class FilledButton extends ButtonStyleButton {
131165
bool? autofocus,
132166
Clip? clipBehavior,
133167
MaterialStatesController? statesController,
134-
required Widget icon,
168+
Widget? icon,
135169
required Widget label,
136170
}) {
171+
if (icon == null) {
172+
return FilledButton.tonal(
173+
key: key,
174+
onPressed: onPressed,
175+
onLongPress: onLongPress,
176+
onHover: onHover,
177+
onFocusChange: onFocusChange,
178+
style: style,
179+
focusNode: focusNode,
180+
autofocus: autofocus ?? false,
181+
clipBehavior: clipBehavior ?? Clip.none,
182+
statesController: statesController,
183+
child: label,
184+
);
185+
}
137186
return _FilledButtonWithIcon.tonal(
138187
key: key,
139188
onPressed: onPressed,

packages/flutter/lib/src/material/outlined_button.dart

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,9 @@ class OutlinedButton extends ButtonStyleButton {
8585
///
8686
/// The icon and label are arranged in a row and padded by 12 logical pixels
8787
/// at the start, and 16 at the end, with an 8 pixel gap in between.
88-
factory OutlinedButton.icon({
88+
///
89+
/// If [icon] is null, will create an [OutlinedButton] instead.
90+
factory OutlinedButton.icon({
8991
Key? key,
9092
required VoidCallback? onPressed,
9193
VoidCallback? onLongPress,
@@ -94,9 +96,35 @@ class OutlinedButton extends ButtonStyleButton {
9496
bool? autofocus,
9597
Clip? clipBehavior,
9698
MaterialStatesController? statesController,
97-
required Widget icon,
99+
Widget? icon,
98100
required Widget label,
99-
}) = _OutlinedButtonWithIcon;
101+
}) {
102+
if (icon == null) {
103+
return OutlinedButton(
104+
key: key,
105+
onPressed: onPressed,
106+
onLongPress: onLongPress,
107+
style: style,
108+
focusNode: focusNode,
109+
autofocus: autofocus ?? false,
110+
clipBehavior: clipBehavior ?? Clip.none,
111+
statesController: statesController,
112+
child: label,
113+
);
114+
}
115+
return _OutlinedButtonWithIcon(
116+
key: key,
117+
onPressed: onPressed,
118+
onLongPress: onLongPress,
119+
style: style,
120+
focusNode: focusNode,
121+
autofocus: autofocus ?? false,
122+
clipBehavior: clipBehavior ?? Clip.none,
123+
statesController: statesController,
124+
icon: icon,
125+
label: label,
126+
);
127+
}
100128

101129
/// A static convenience method that constructs an outlined button
102130
/// [ButtonStyle] given simple values.

packages/flutter/lib/src/material/text_button.dart

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,38 @@ class TextButton extends ButtonStyleButton {
105105
bool? autofocus,
106106
Clip? clipBehavior,
107107
MaterialStatesController? statesController,
108-
required Widget icon,
108+
Widget? icon,
109109
required Widget label,
110-
}) = _TextButtonWithIcon;
110+
}) {
111+
if (icon == null) {
112+
return TextButton(
113+
key: key,
114+
onPressed: onPressed,
115+
onLongPress: onLongPress,
116+
onHover: onHover,
117+
onFocusChange: onFocusChange,
118+
style: style,
119+
focusNode: focusNode,
120+
autofocus: autofocus ?? false,
121+
clipBehavior: clipBehavior ?? Clip.none,
122+
statesController: statesController,
123+
child: label,
124+
);
125+
}
126+
return _TextButtonWithIcon( key: key,
127+
onPressed: onPressed,
128+
onLongPress: onLongPress,
129+
onHover: onHover,
130+
onFocusChange: onFocusChange,
131+
style: style,
132+
focusNode: focusNode,
133+
autofocus: autofocus ?? false,
134+
clipBehavior: clipBehavior ?? Clip.none,
135+
statesController: statesController,
136+
icon: icon,
137+
label: label,
138+
);
139+
}
111140

112141
/// A static convenience method that constructs a text button
113142
/// [ButtonStyle] given simple values.

packages/flutter/test/material/elevated_button_test.dart

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,45 @@ void main() {
159159
expect(material.type, MaterialType.button);
160160
});
161161

162+
testWidgets('ElevatedButton.icon produces the correct widgets if icon is null', (WidgetTester tester) async {
163+
const ColorScheme colorScheme = ColorScheme.light();
164+
final ThemeData theme = ThemeData.from(colorScheme: colorScheme);
165+
final Key iconButtonKey = UniqueKey();
166+
await tester.pumpWidget(
167+
MaterialApp(
168+
theme: theme,
169+
home: Center(
170+
child: ElevatedButton.icon(
171+
key: iconButtonKey,
172+
onPressed: () { },
173+
icon: const Icon(Icons.add),
174+
label: const Text('label'),
175+
),
176+
),
177+
),
178+
);
179+
180+
expect(find.byIcon(Icons.add), findsOneWidget);
181+
expect(find.text('label'), findsOneWidget);
182+
183+
await tester.pumpWidget(
184+
MaterialApp(
185+
theme: theme,
186+
home: Center(
187+
child: ElevatedButton.icon(
188+
key: iconButtonKey,
189+
onPressed: () { },
190+
// No icon specified.
191+
label: const Text('label'),
192+
),
193+
),
194+
),
195+
);
196+
197+
expect(find.byIcon(Icons.add), findsNothing);
198+
expect(find.text('label'), findsOneWidget);
199+
});
200+
162201
testWidgets('Default ElevatedButton meets a11y contrast guidelines', (WidgetTester tester) async {
163202
final FocusNode focusNode = FocusNode();
164203

packages/flutter/test/material/filled_button_test.dart

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,84 @@ void main() {
127127
await tester.pumpAndSettle();
128128
});
129129

130+
testWidgets('FilledButton.icon produces the correct widgets if icon is null', (WidgetTester tester) async {
131+
const ColorScheme colorScheme = ColorScheme.light();
132+
final ThemeData theme = ThemeData.from(colorScheme: colorScheme);
133+
final Key iconButtonKey = UniqueKey();
134+
await tester.pumpWidget(
135+
MaterialApp(
136+
theme: theme,
137+
home: Center(
138+
child: FilledButton.icon(
139+
key: iconButtonKey,
140+
onPressed: () { },
141+
icon: const Icon(Icons.add),
142+
label: const Text('label'),
143+
),
144+
),
145+
),
146+
);
147+
148+
expect(find.byIcon(Icons.add), findsOneWidget);
149+
expect(find.text('label'), findsOneWidget);
150+
151+
await tester.pumpWidget(
152+
MaterialApp(
153+
theme: theme,
154+
home: Center(
155+
child: FilledButton.icon(
156+
key: iconButtonKey,
157+
onPressed: () { },
158+
// No icon specified.
159+
label: const Text('label'),
160+
),
161+
),
162+
),
163+
);
164+
165+
expect(find.byIcon(Icons.add), findsNothing);
166+
expect(find.text('label'), findsOneWidget);
167+
});
168+
169+
testWidgets('FilledButton.tonalIcon produces the correct widgets if icon is null', (WidgetTester tester) async {
170+
const ColorScheme colorScheme = ColorScheme.light();
171+
final ThemeData theme = ThemeData.from(colorScheme: colorScheme);
172+
final Key iconButtonKey = UniqueKey();
173+
await tester.pumpWidget(
174+
MaterialApp(
175+
theme: theme,
176+
home: Center(
177+
child: FilledButton.tonalIcon(
178+
key: iconButtonKey,
179+
onPressed: () { },
180+
icon: const Icon(Icons.add),
181+
label: const Text('label'),
182+
),
183+
),
184+
),
185+
);
186+
187+
expect(find.byIcon(Icons.add), findsOneWidget);
188+
expect(find.text('label'), findsOneWidget);
189+
190+
await tester.pumpWidget(
191+
MaterialApp(
192+
theme: theme,
193+
home: Center(
194+
child: FilledButton.tonalIcon(
195+
key: iconButtonKey,
196+
onPressed: () { },
197+
// No icon specified.
198+
label: const Text('label'),
199+
),
200+
),
201+
),
202+
);
203+
204+
expect(find.byIcon(Icons.add), findsNothing);
205+
expect(find.text('label'), findsOneWidget);
206+
});
207+
130208
testWidgets('FilledButton.tonal, FilledButton.tonalIcon defaults', (WidgetTester tester) async {
131209
const ColorScheme colorScheme = ColorScheme.light();
132210
final ThemeData theme = ThemeData.from(colorScheme: colorScheme);

0 commit comments

Comments
 (0)