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

diff --git a/dev/tools/gen_defaults/lib/icon_button_template.dart b/dev/tools/gen_defaults/lib/icon_button_template.dart
index bb1d394..1fbd304 100644
--- a/dev/tools/gen_defaults/lib/icon_button_template.dart
+++ b/dev/tools/gen_defaults/lib/icon_button_template.dart
@@ -35,12 +35,26 @@
       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')};
       }
diff --git a/examples/api/lib/material/icon_button/icon_button.3.dart b/examples/api/lib/material/icon_button/icon_button.3.dart
new file mode 100644
index 0000000..230d292
--- /dev/null
+++ b/examples/api/lib/material/icon_button/icon_button.3.dart
@@ -0,0 +1,192 @@
+// 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)),
+  );
+}
diff --git a/packages/flutter/lib/src/material/icon_button.dart b/packages/flutter/lib/src/material/icon_button.dart
index 95a606a..a11b66a 100644
--- a/packages/flutter/lib/src/material/icon_button.dart
+++ b/packages/flutter/lib/src/material/icon_button.dart
@@ -101,6 +101,14 @@
 /// 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 @@
 /// ** 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 @@
     this.enableFeedback = true,
     this.constraints,
     this.style,
+    this.isSelected,
+    this.selectedIcon,
     required this.icon,
   }) : assert(padding != null),
        assert(alignment != null),
@@ -218,12 +236,34 @@
   /// 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 @@
   /// 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 @@
   /// 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 @@
   /// 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 @@
         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 @@
           child: iconButton,
         );
       }
-      return _IconButtonM3(
+
+      return _SelectableIconButton(
         style: adjustedStyle,
         onPressed: onPressed,
         autofocus: autofocus,
         focusNode: focusNode,
+        isSelected: isSelected,
         child: iconButton,
       );
     }
@@ -574,12 +660,76 @@
   }
 }
 
+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 @@
   /// * `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 @@
 
   @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 @@
       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);
       }
diff --git a/packages/flutter/test/material/icon_button_test.dart b/packages/flutter/test/material/icon_button_test.dart
index ae0354d..b8c07e6 100644
--- a/packages/flutter/test/material/icon_button_test.dart
+++ b/packages/flutter/test/material/icon_button_test.dart
@@ -1034,7 +1034,7 @@
     expect(material.textStyle, null);
     expect(material.type, MaterialType.button);
 
-    // Disabled TextButton
+    // Disabled IconButton
     await tester.pumpWidget(
       MaterialApp(
         theme: themeM3,
@@ -1108,12 +1108,14 @@
   );
 
   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 @@
       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 @@
     // 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 @@
     );
     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}) {