Unverified Commit 16bbef18 authored by Qun Cheng's avatar Qun Cheng Committed by GitHub

Migrate `IconButton` to Material 3 - Part 2 (#106437)

parent 5a5721c9
......@@ -35,12 +35,26 @@ class _TokenDefaultsM3 extends ButtonStyle {
if (states.contains(MaterialState.disabled)) {
return ${componentColor('md.comp.icon-button.disabled.icon')};
}
if (states.contains(MaterialState.selected)) {
return ${componentColor('md.comp.icon-button.selected.icon')};
}
return ${componentColor('md.comp.icon-button.unselected.icon')};
});
@override
MaterialStateProperty<Color?>? get overlayColor =>
MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
if (states.contains(MaterialState.hovered)) {
return ${componentColor('md.comp.icon-button.selected.hover.state-layer')};
}
if (states.contains(MaterialState.focused)) {
return ${componentColor('md.comp.icon-button.selected.focus.state-layer')};
}
if (states.contains(MaterialState.pressed)) {
return ${componentColor('md.comp.icon-button.selected.pressed.state-layer')};
}
}
if (states.contains(MaterialState.hovered)) {
return ${componentColor('md.comp.icon-button.unselected.hover.state-layer')};
}
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flutter code sample for IconButton with toggle feature
import 'package:flutter/material.dart';
void main() {
runApp(const IconButtonToggleApp());
}
class IconButtonToggleApp extends StatelessWidget {
const IconButtonToggleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
colorSchemeSeed: const Color(0xff6750a4),
useMaterial3: true,
// Desktop and web platforms have a compact visual density by default.
// To see buttons with circular background on desktop/web, the "visualDensity"
// needs to be set to "VisualDensity.standard".
visualDensity: VisualDensity.standard,
),
title: 'Icon Button Types',
home: const Scaffold(
body: DemoIconToggleButtons(),
),
);
}
}
class DemoIconToggleButtons extends StatefulWidget {
const DemoIconToggleButtons({super.key});
@override
State<DemoIconToggleButtons> createState() => _DemoIconToggleButtonsState();
}
class _DemoIconToggleButtonsState extends State<DemoIconToggleButtons> {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
// Standard IconButton
children: const <Widget>[
DemoIconToggleButton(isEnabled: true),
SizedBox(width: 10),
DemoIconToggleButton(isEnabled: false),
]
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const <Widget>[
// Filled IconButton
DemoIconToggleButton(isEnabled: true, getDefaultStyle: enabledFilledButtonStyle,),
SizedBox(width: 10),
DemoIconToggleButton(isEnabled: false, getDefaultStyle: disabledFilledButtonStyle,)
]
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const <Widget>[
// Filled Tonal IconButton
DemoIconToggleButton(isEnabled: true, getDefaultStyle: enabledFilledTonalButtonStyle,),
SizedBox(width: 10),
DemoIconToggleButton(isEnabled: false, getDefaultStyle: disabledFilledTonalButtonStyle,),
]
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const <Widget>[
// Outlined IconButton
DemoIconToggleButton(isEnabled: true, getDefaultStyle: enabledOutlinedButtonStyle,),
SizedBox(width: 10),
DemoIconToggleButton(isEnabled: false, getDefaultStyle: disabledOutlinedButtonStyle,),
]
),
]
),
);
}
}
class DemoIconToggleButton extends StatefulWidget {
const DemoIconToggleButton({required this.isEnabled, this.getDefaultStyle, super.key});
final bool isEnabled;
final ButtonStyle? Function(bool, ColorScheme)? getDefaultStyle;
@override
State<DemoIconToggleButton> createState() => _DemoIconToggleButtonState();
}
class _DemoIconToggleButtonState extends State<DemoIconToggleButton> {
bool selected = false;
@override
Widget build(BuildContext context) {
final ColorScheme colors = Theme.of(context).colorScheme;
final VoidCallback? onPressed = widget.isEnabled
? () {
setState(() {
selected = !selected;
});
}
: null;
ButtonStyle? style;
if (widget.getDefaultStyle != null) {
style = widget.getDefaultStyle!(selected, colors);
}
return IconButton(
isSelected: selected,
icon: const Icon(Icons.settings_outlined),
selectedIcon: const Icon(Icons.settings),
onPressed: onPressed,
style: style,
);
}
}
ButtonStyle enabledFilledButtonStyle(bool selected, ColorScheme colors) {
return IconButton.styleFrom(
foregroundColor: selected ? colors.onPrimary : colors.primary,
backgroundColor: selected ? colors.primary : colors.surfaceVariant,
disabledForegroundColor: colors.onSurface.withOpacity(0.38),
disabledBackgroundColor: colors.onSurface.withOpacity(0.12),
hoverColor: selected ? colors.onPrimary.withOpacity(0.08) : colors.primary.withOpacity(0.08),
focusColor: selected ? colors.onPrimary.withOpacity(0.12) : colors.primary.withOpacity(0.12),
highlightColor: selected ? colors.onPrimary.withOpacity(0.12) : colors.primary.withOpacity(0.12),
);
}
ButtonStyle disabledFilledButtonStyle(bool selected, ColorScheme colors) {
return IconButton.styleFrom(
disabledForegroundColor: colors.onSurface.withOpacity(0.38),
disabledBackgroundColor: colors.onSurface.withOpacity(0.12),
);
}
ButtonStyle enabledFilledTonalButtonStyle(bool selected, ColorScheme colors) {
return IconButton.styleFrom(
foregroundColor: selected ? colors.onSecondaryContainer : colors.onSurfaceVariant,
backgroundColor: selected ? colors.secondaryContainer : colors.surfaceVariant,
hoverColor: selected ? colors.onSecondaryContainer.withOpacity(0.08) : colors.onSurfaceVariant.withOpacity(0.08),
focusColor: selected ? colors.onSecondaryContainer.withOpacity(0.12) : colors.onSurfaceVariant.withOpacity(0.12),
highlightColor: selected ? colors.onSecondaryContainer.withOpacity(0.12) : colors.onSurfaceVariant.withOpacity(0.12),
);
}
ButtonStyle disabledFilledTonalButtonStyle(bool selected, ColorScheme colors) {
return IconButton.styleFrom(
disabledForegroundColor: colors.onSurface.withOpacity(0.38),
disabledBackgroundColor: colors.onSurface.withOpacity(0.12),
);
}
ButtonStyle enabledOutlinedButtonStyle(bool selected, ColorScheme colors) {
return IconButton.styleFrom(
backgroundColor: selected ? colors.inverseSurface : null,
hoverColor: selected ? colors.onInverseSurface.withOpacity(0.08) : colors.onSurfaceVariant.withOpacity(0.08),
focusColor: selected ? colors.onInverseSurface.withOpacity(0.12) : colors.onSurfaceVariant.withOpacity(0.12),
highlightColor: selected ? colors.onInverseSurface.withOpacity(0.12) : colors.onSurface.withOpacity(0.12),
side: BorderSide(color: colors.outline),
).copyWith(
foregroundColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
return colors.onInverseSurface;
}
if (states.contains(MaterialState.pressed)) {
return colors.onSurface;
}
return null;
}),
);
}
ButtonStyle disabledOutlinedButtonStyle(bool selected, ColorScheme colors) {
return IconButton.styleFrom(
disabledForegroundColor: colors.onSurface.withOpacity(0.38),
disabledBackgroundColor: selected ? colors.onSurface.withOpacity(0.12) : null,
side: selected ? null : BorderSide(color: colors.outline.withOpacity(0.12)),
);
}
......@@ -101,6 +101,14 @@ const double _kMinButtonSize = kMinInteractiveDimension;
/// The default [IconButton] is the standard type, and contained icon buttons can be produced
/// by configuring the [IconButton] widget's properties.
///
/// Material Design 3 also treats [IconButton]s as toggle buttons. In order
/// to not break existing apps, the toggle feature can be optionally controlled
/// by the [isSelected] property.
///
/// If [isSelected] is null it will behave as a normal button. If [isSelected] is not
/// null then it will behave as a toggle button. If [isSelected] is true then it will
/// show [selectedIcon], if it false it will show the normal [icon].
///
/// {@tool dartpad}
/// This sample shows creation of [IconButton] widgets for standard, filled,
/// filled tonal and outlined types, as described in: https://m3.material.io/components/icon-buttons/overview
......@@ -108,6 +116,14 @@ const double _kMinButtonSize = kMinInteractiveDimension;
/// ** See code in examples/api/lib/material/icon_button/icon_button.2.dart **
/// {@end-tool}
///
/// {@tool dartpad}
/// This sample shows creation of [IconButton] widgets with toggle feature for
/// standard, filled, filled tonal and outlined types, as described
/// in: https://m3.material.io/components/icon-buttons/overview
///
/// ** See code in examples/api/lib/material/icon_button/icon_button.3.dart **
/// {@end-tool}
///
/// See also:
///
/// * [Icons], the library of Material Icons.
......@@ -151,6 +167,8 @@ class IconButton extends StatelessWidget {
this.enableFeedback = true,
this.constraints,
this.style,
this.isSelected,
this.selectedIcon,
required this.icon,
}) : assert(padding != null),
assert(alignment != null),
......@@ -218,12 +236,34 @@ class IconButton extends StatelessWidget {
/// See [Icon], [ImageIcon].
final Widget icon;
/// The color for the button's icon when it has the input focus.
/// The color for the button when it has the input focus.
///
/// If [ThemeData.useMaterial3] is set to true, this [focusColor] will be mapped
/// to be the [ButtonStyle.overlayColor] in focused state, which paints on top of
/// the button, as an overlay. Therefore, using a color with some transparency
/// is recommended. For example, one could customize the [focusColor] below:
///
/// ```dart
/// IconButton(
/// focusColor: Colors.orange.withOpacity(0.3),
/// )
/// ```
///
/// Defaults to [ThemeData.focusColor] of the ambient theme.
final Color? focusColor;
/// The color for the button's icon when a pointer is hovering over it.
/// The color for the button when a pointer is hovering over it.
///
/// If [ThemeData.useMaterial3] is set to true, this [hoverColor] will be mapped
/// to be the [ButtonStyle.overlayColor] in hovered state, which paints on top of
/// the button, as an overlay. Therefore, using a color with some transparency
/// is recommended. For example, one could customize the [hoverColor] below:
///
/// ```dart
/// IconButton(
/// hoverColor: Colors.orange.withOpacity(0.3),
/// )
/// ```
///
/// Defaults to [ThemeData.hoverColor] of the ambient theme.
final Color? hoverColor;
......@@ -249,7 +289,9 @@ class IconButton extends StatelessWidget {
/// fill the button area if the touch is held for long enough time. If the splash
/// color has transparency then the highlight and button color will show through.
///
/// If [ThemeData.useMaterial3] is set to true, this will not be used.
/// If [ThemeData.useMaterial3] is set to true, this will not be used. Use
/// [highlightColor] instead to show the overlay color of the button when the button
/// is in the pressed state.
///
/// Defaults to the Theme's splash color, [ThemeData.splashColor].
final Color? splashColor;
......@@ -259,6 +301,17 @@ class IconButton extends StatelessWidget {
/// button color (if any). If the highlight color has transparency, the button color
/// will show through. The highlight fades in quickly as the button is held down.
///
/// If [ThemeData.useMaterial3] is set to true, this [highlightColor] will be mapped
/// to be the [ButtonStyle.overlayColor] in pressed state, which paints on top
/// of the button, as an overlay. Therefore, using a color with some transparency
/// is recommended. For example, one could customize the [highlightColor] below:
///
/// ```dart
/// IconButton(
/// highlightColor: Colors.orange.withOpacity(0.3),
/// )
/// ```
///
/// Defaults to the Theme's highlight color, [ThemeData.highlightColor].
final Color? highlightColor;
......@@ -341,6 +394,32 @@ class IconButton extends StatelessWidget {
/// Null by default.
final ButtonStyle? style;
/// The optional selection state of the icon button.
///
/// If this property is null, the button will behave as a normal push button,
/// otherwise, the button will toggle between showing [icon] and [selectedIcon]
/// based on the value of [isSelected]. If true, it will show [selectedIcon],
/// if false it will show [icon].
///
/// This property is only used if [ThemeData.useMaterial3] is true.
final bool? isSelected;
/// The icon to display inside the button when [isSelected] is true. This property
/// can be null. The original [icon] will be used for both selected and unselected
/// status if it is null.
///
/// The [Icon.size] and [Icon.color] of the icon is configured automatically
/// based on the [iconSize] and [color] properties using an [IconTheme] and
/// therefore should not be explicitly configured in the icon widget.
///
/// This property is only used if [ThemeData.useMaterial3] is true.
///
/// See also:
///
/// * [Icon], for icons based on glyphs from fonts instead of images.
/// * [ImageIcon], for showing icons from [AssetImage]s or other [ImageProvider]s.
final Widget? selectedIcon;
/// A static convenience method that constructs an icon button
/// [ButtonStyle] given simple values. This method is only used for Material 3.
///
......@@ -484,11 +563,16 @@ class IconButton extends StatelessWidget {
adjustedStyle = style!.merge(adjustedStyle);
}
Widget effectiveIcon = icon;
if ((isSelected ?? false) && selectedIcon != null) {
effectiveIcon = selectedIcon!;
}
Widget iconButton = IconTheme.merge(
data: IconThemeData(
size: effectiveIconSize,
),
child: icon,
child: effectiveIcon,
);
if (tooltip != null) {
iconButton = Tooltip(
......@@ -496,11 +580,13 @@ class IconButton extends StatelessWidget {
child: iconButton,
);
}
return _IconButtonM3(
return _SelectableIconButton(
style: adjustedStyle,
onPressed: onPressed,
autofocus: autofocus,
focusNode: focusNode,
isSelected: isSelected,
child: iconButton,
);
}
......@@ -574,12 +660,76 @@ class IconButton extends StatelessWidget {
}
}
class _SelectableIconButton extends StatefulWidget {
const _SelectableIconButton({
this.isSelected,
this.style,
this.focusNode,
required this.autofocus,
required this.onPressed,
required this.child,
});
final bool? isSelected;
final ButtonStyle? style;
final FocusNode? focusNode;
final bool autofocus;
final VoidCallback? onPressed;
final Widget child;
@override
State<_SelectableIconButton> createState() => _SelectableIconButtonState();
}
class _SelectableIconButtonState extends State<_SelectableIconButton> {
late final MaterialStatesController statesController;
@override
void initState() {
super.initState();
if (widget.isSelected == null) {
statesController = MaterialStatesController();
} else {
statesController = MaterialStatesController(<MaterialState>{
if (widget.isSelected!) MaterialState.selected
});
}
}
@override
void didUpdateWidget(_SelectableIconButton oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.isSelected == null) {
if (statesController.value.contains(MaterialState.selected)) {
statesController.update(MaterialState.selected, false);
}
return;
}
if (widget.isSelected != oldWidget.isSelected) {
statesController.update(MaterialState.selected, widget.isSelected!);
}
}
@override
Widget build(BuildContext context) {
return _IconButtonM3(
statesController: statesController,
style: widget.style,
autofocus: widget.autofocus,
focusNode: widget.focusNode,
onPressed: widget.onPressed,
child: widget.child,
);
}
}
class _IconButtonM3 extends ButtonStyleButton {
const _IconButtonM3({
required super.onPressed,
super.style,
super.focusNode,
super.autofocus = false,
super.statesController,
required Widget super.child,
}) : super(
onLongPress: null,
......@@ -596,8 +746,12 @@ class _IconButtonM3 extends ButtonStyleButton {
/// * `backgroundColor` - transparent
/// * `foregroundColor`
/// * disabled - Theme.colorScheme.onSurface(0.38)
/// * selected - Theme.colorScheme.primary
/// * others - Theme.colorScheme.onSurfaceVariant
/// * `overlayColor`
/// * selected
/// * hovered - Theme.colorScheme.primary(0.08)
/// * focused or pressed - Theme.colorScheme.primary(0.12)
/// * hovered or focused - Theme.colorScheme.onSurfaceVariant(0.08)
/// * pressed - Theme.colorScheme.onSurfaceVariant(0.12)
/// * others - null
......@@ -684,15 +838,26 @@ class _IconButtonDefaultOverlay extends MaterialStateProperty<Color?> {
@override
Color? resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
if (states.contains(MaterialState.pressed)) {
return highlightColor ?? foregroundColor?.withOpacity(0.12);
}
if (states.contains(MaterialState.hovered)) {
return hoverColor ?? foregroundColor?.withOpacity(0.08);
}
if (states.contains(MaterialState.focused)) {
return focusColor ?? foregroundColor?.withOpacity(0.12);
}
}
if (states.contains(MaterialState.pressed)) {
return highlightColor ?? foregroundColor?.withOpacity(0.12);
}
if (states.contains(MaterialState.hovered)) {
return hoverColor ?? foregroundColor?.withOpacity(0.08);
}
if (states.contains(MaterialState.focused)) {
return focusColor ?? foregroundColor?.withOpacity(0.08);
}
if (states.contains(MaterialState.pressed)) {
return highlightColor ?? foregroundColor?.withOpacity(0.12);
}
return null;
}
......@@ -748,12 +913,26 @@ class _TokenDefaultsM3 extends ButtonStyle {
if (states.contains(MaterialState.disabled)) {
return _colors.onSurface.withOpacity(0.38);
}
if (states.contains(MaterialState.selected)) {
return _colors.primary;
}
return _colors.onSurfaceVariant;
});
@override
MaterialStateProperty<Color?>? get overlayColor =>
MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
if (states.contains(MaterialState.hovered)) {
return _colors.primary.withOpacity(0.08);
}
if (states.contains(MaterialState.focused)) {
return _colors.primary.withOpacity(0.12);
}
if (states.contains(MaterialState.pressed)) {
return _colors.primary.withOpacity(0.12);
}
}
if (states.contains(MaterialState.hovered)) {
return _colors.onSurfaceVariant.withOpacity(0.08);
}
......
......@@ -1034,7 +1034,7 @@ void main() {
expect(material.textStyle, null);
expect(material.type, MaterialType.button);
// Disabled TextButton
// Disabled IconButton
await tester.pumpWidget(
MaterialApp(
theme: themeM3,
......@@ -1108,12 +1108,14 @@ void main() {
);
testWidgets('IconButton uses stateful color for icon color in different states - M3', (WidgetTester tester) async {
bool isSelected = false;
final FocusNode focusNode = FocusNode();
const Color pressedColor = Color(0x00000001);
const Color hoverColor = Color(0x00000002);
const Color focusedColor = Color(0x00000003);
const Color defaultColor = Color(0x00000004);
const Color selectedColor = Color(0x00000005);
Color getIconColor(Set<MaterialState> states) {
if (states.contains(MaterialState.pressed)) {
......@@ -1125,23 +1127,35 @@ void main() {
if (states.contains(MaterialState.focused)) {
return focusedColor;
}
if (states.contains(MaterialState.selected)) {
return selectedColor;
}
return defaultColor;
}
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(colorScheme: const ColorScheme.light(), useMaterial3: true),
home: Scaffold(
body: Center(
child: IconButton(
style: ButtonStyle(
foregroundColor: MaterialStateProperty.resolveWith<Color>(getIconColor),
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Scaffold(
body: Center(
child: IconButton(
style: ButtonStyle(
foregroundColor: MaterialStateProperty.resolveWith<Color>(getIconColor),
),
isSelected: isSelected,
onPressed: () {
setState(() {
isSelected = !isSelected;
});
},
focusNode: focusNode,
icon: const Icon(Icons.ac_unit),
),
),
onPressed: () {},
focusNode: focusNode,
icon: const Icon(Icons.ac_unit),
),
),
);
}
),
),
);
......@@ -1151,6 +1165,12 @@ void main() {
// Default, not disabled.
expect(iconColor(), equals(defaultColor));
// Selected
final Finder button = find.byType(IconButton);
await tester.tap(button);
await tester.pumpAndSettle();
expect(iconColor(), selectedColor);
// Focused.
focusNode.requestFocus();
await tester.pumpAndSettle();
......@@ -1319,6 +1339,236 @@ void main() {
);
expect(paddingWidget3.padding, const EdgeInsets.all(22));
});
testWidgets('Default IconButton is not selectable - M3', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(colorScheme: const ColorScheme.light(), useMaterial3: true),
home: IconButton(icon: const Icon(Icons.ac_unit), onPressed: (){},)
)
);
final Finder button = find.byType(IconButton);
IconButton buttonWidget() => tester.widget<IconButton>(button);
Material buttonMaterial() {
return tester.widget<Material>(
find.descendant(
of: find.byType(IconButton),
matching: find.byType(Material),
)
);
}
Color? iconColor() => _iconStyle(tester, Icons.ac_unit)?.color;
expect(buttonWidget().isSelected, null);
expect(iconColor(), equals(const ColorScheme.light().onSurfaceVariant));
expect(buttonMaterial().color, Colors.transparent);
await tester.tap(button); // The non-toggle IconButton should not change appearance after clicking
await tester.pumpAndSettle();
expect(buttonWidget().isSelected, null);
expect(iconColor(), equals(const ColorScheme.light().onSurfaceVariant));
expect(buttonMaterial().color, Colors.transparent);
});
testWidgets('Icon button is selectable when isSelected is not null - M3', (WidgetTester tester) async {
bool isSelected = false;
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(colorScheme: const ColorScheme.light(), useMaterial3: true),
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return IconButton(
isSelected: isSelected,
icon: const Icon(Icons.ac_unit),
onPressed: (){
setState(() {
isSelected = !isSelected;
});
},
);
}
)
)
);
final Finder button = find.byType(IconButton);
IconButton buttonWidget() => tester.widget<IconButton>(button);
Color? iconColor() => _iconStyle(tester, Icons.ac_unit)?.color;
Material buttonMaterial() {
return tester.widget<Material>(
find.descendant(
of: find.byType(IconButton),
matching: find.byType(Material),
)
);
}
expect(buttonWidget().isSelected, false);
expect(iconColor(), equals(const ColorScheme.light().onSurfaceVariant));
expect(buttonMaterial().color, Colors.transparent);
await tester.tap(button); // The toggle IconButton should change appearance after clicking
await tester.pumpAndSettle();
expect(buttonWidget().isSelected, true);
expect(iconColor(), equals(const ColorScheme.light().primary));
expect(buttonMaterial().color, Colors.transparent);
await tester.tap(button); // The IconButton should be unselected if it's clicked again
await tester.pumpAndSettle();
expect(buttonWidget().isSelected, false);
expect(iconColor(), equals(const ColorScheme.light().onSurfaceVariant));
expect(buttonMaterial().color, Colors.transparent);
});
testWidgets('The IconButton is in selected status if isSelected is true by default - M3', (WidgetTester tester) async {
bool isSelected = true;
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(colorScheme: const ColorScheme.light(), useMaterial3: true),
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return IconButton(
isSelected: isSelected,
icon: const Icon(Icons.ac_unit),
onPressed: (){
setState(() {
isSelected = !isSelected;
});
},
);
}
)
)
);
final Finder button = find.byType(IconButton);
IconButton buttonWidget() => tester.widget<IconButton>(button);
Color? iconColor() => _iconStyle(tester, Icons.ac_unit)?.color;
Material buttonMaterial() {
return tester.widget<Material>(
find.descendant(
of: find.byType(IconButton),
matching: find.byType(Material),
)
);
}
expect(buttonWidget().isSelected, true);
expect(iconColor(), equals(const ColorScheme.light().primary));
expect(buttonMaterial().color, Colors.transparent);
await tester.tap(button); // The IconButton becomes unselected if it's clicked
await tester.pumpAndSettle();
expect(buttonWidget().isSelected, false);
expect(iconColor(), equals(const ColorScheme.light().onSurfaceVariant));
expect(buttonMaterial().color, Colors.transparent);
});
testWidgets("The selectedIcon is used if it's not null and the button is clicked" , (WidgetTester tester) async {
bool isSelected = false;
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(colorScheme: const ColorScheme.light(), useMaterial3: true),
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return IconButton(
isSelected: isSelected,
selectedIcon: const Icon(Icons.account_box),
icon: const Icon(Icons.account_box_outlined),
onPressed: (){
setState(() {
isSelected = !isSelected;
});
},
);
}
)
)
);
final Finder button = find.byType(IconButton);
expect(find.byIcon(Icons.account_box_outlined), findsOneWidget);
expect(find.byIcon(Icons.account_box), findsNothing);
await tester.tap(button); // The icon becomes to selectedIcon
await tester.pumpAndSettle();
expect(find.byIcon(Icons.account_box), findsOneWidget);
expect(find.byIcon(Icons.account_box_outlined), findsNothing);
await tester.tap(button); // The icon becomes the original icon when it's clicked again
await tester.pumpAndSettle();
expect(find.byIcon(Icons.account_box_outlined), findsOneWidget);
expect(find.byIcon(Icons.account_box), findsNothing);
});
testWidgets('The original icon is used for selected and unselected status when selectedIcon is null' , (WidgetTester tester) async {
bool isSelected = false;
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(colorScheme: const ColorScheme.light(), useMaterial3: true),
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return IconButton(
isSelected: isSelected,
icon: const Icon(Icons.account_box),
onPressed: (){
setState(() {
isSelected = !isSelected;
});
},
);
}
)
)
);
final Finder button = find.byType(IconButton);
IconButton buttonWidget() => tester.widget<IconButton>(button);
expect(buttonWidget().isSelected, false);
expect(buttonWidget().selectedIcon, null);
expect(find.byIcon(Icons.account_box), findsOneWidget);
await tester.tap(button); // The icon becomes the original icon when it's clicked again
await tester.pumpAndSettle();
expect(buttonWidget().isSelected, true);
expect(buttonWidget().selectedIcon, null);
expect(find.byIcon(Icons.account_box), findsOneWidget);
});
testWidgets('The selectedIcon is used for disabled button if isSelected is true - M3' , (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData.from(colorScheme: const ColorScheme.light(), useMaterial3: true),
home: const IconButton(
isSelected: true,
icon: Icon(Icons.account_box),
selectedIcon: Icon(Icons.ac_unit),
onPressed: null,
)
)
);
final Finder button = find.byType(IconButton);
IconButton buttonWidget() => tester.widget<IconButton>(button);
expect(buttonWidget().isSelected, true);
expect(find.byIcon(Icons.account_box), findsNothing);
expect(find.byIcon(Icons.ac_unit), findsOneWidget);
});
}
Widget wrap({required Widget child, required bool useMaterial3}) {
......
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