Allow IconButton to have smaller sizes (#47457)

diff --git a/packages/flutter/lib/src/material/button.dart b/packages/flutter/lib/src/material/button.dart
index 8862793..43e8730 100644
--- a/packages/flutter/lib/src/material/button.dart
+++ b/packages/flutter/lib/src/material/button.dart
@@ -366,10 +366,7 @@
     final Color effectiveTextColor = MaterialStateProperty.resolveAs<Color>(widget.textStyle?.color, _states);
     final ShapeBorder effectiveShape =  MaterialStateProperty.resolveAs<ShapeBorder>(widget.shape, _states);
     final Offset densityAdjustment = widget.visualDensity.baseSizeAdjustment;
-    final BoxConstraints effectiveConstraints = widget.constraints.copyWith(
-      minWidth: widget.constraints.minWidth != null ? (widget.constraints.minWidth + densityAdjustment.dx).clamp(0.0, double.infinity) as double : null,
-      minHeight: widget.constraints.minWidth != null ? (widget.constraints.minHeight + densityAdjustment.dy).clamp(0.0, double.infinity) as double : null,
-    );
+    final BoxConstraints effectiveConstraints = widget.visualDensity.effectiveConstraints(widget.constraints);
     final EdgeInsetsGeometry padding = widget.padding.add(
       EdgeInsets.only(
         left: densityAdjustment.dx,
diff --git a/packages/flutter/lib/src/material/icon_button.dart b/packages/flutter/lib/src/material/icon_button.dart
index 6b3ce2f..2d510da 100644
--- a/packages/flutter/lib/src/material/icon_button.dart
+++ b/packages/flutter/lib/src/material/icon_button.dart
@@ -154,6 +154,7 @@
     this.autofocus = false,
     this.tooltip,
     this.enableFeedback = true,
+    this.constraints,
   }) : assert(iconSize != null),
        assert(padding != null),
        assert(alignment != null),
@@ -288,6 +289,26 @@
   ///  * [Feedback] for providing platform-specific feedback to certain actions.
   final bool enableFeedback;
 
+  /// Optional size constraints for the button.
+  ///
+  /// When unspecified, defaults to:
+  /// ```dart
+  /// const BoxConstraints(
+  ///   minWidth: kMinInteractiveDimension,
+  ///   minHeight: kMinInteractiveDimension,
+  /// )
+  /// ```
+  /// where [kMinInteractiveDimension] is 48.0, and then with visual density
+  /// applied.
+  ///
+  /// The default constraints ensure that the button is accessible.
+  /// Specifying this parameter enables creation of buttons smaller than
+  /// the minimum size, but it is not recommended.
+  ///
+  /// The visual density uses the [visualDensity] parameter if specified,
+  /// and `Theme.of(context).visualDensity` otherwise.
+  final BoxConstraints constraints;
+
   @override
   Widget build(BuildContext context) {
     assert(debugCheckHasMaterial(context));
@@ -298,9 +319,16 @@
     else
       currentColor = disabledColor ?? theme.disabledColor;
 
-    final Offset densityAdjustment = (visualDensity ?? theme.visualDensity).baseSizeAdjustment;
+    final VisualDensity effectiveVisualDensity = visualDensity ?? theme.visualDensity;
+
+    final BoxConstraints unadjustedConstraints = constraints ?? const BoxConstraints(
+      minWidth: _kMinButtonSize,
+      minHeight: _kMinButtonSize,
+    );
+    final BoxConstraints adjustedConstraints = effectiveVisualDensity.effectiveConstraints(unadjustedConstraints);
+
     Widget result = ConstrainedBox(
-      constraints: BoxConstraints(minWidth: _kMinButtonSize + densityAdjustment.dx, minHeight: _kMinButtonSize + densityAdjustment.dy),
+      constraints: adjustedConstraints,
       child: Padding(
         padding: padding,
         child: SizedBox(
diff --git a/packages/flutter/lib/src/material/theme_data.dart b/packages/flutter/lib/src/material/theme_data.dart
index 5281031..b81f743 100644
--- a/packages/flutter/lib/src/material/theme_data.dart
+++ b/packages/flutter/lib/src/material/theme_data.dart
@@ -1815,6 +1815,16 @@
     );
   }
 
+  /// Return a copy of [constraints] whose minimum width and height have been
+  /// updated with the [baseSizeAdjustment].
+  BoxConstraints effectiveConstraints(BoxConstraints constraints){
+    assert(constraints != null && constraints.debugAssertIsValid());
+    return constraints.copyWith(
+      minWidth: (constraints.minWidth + baseSizeAdjustment.dx).clamp(0.0, double.infinity).toDouble(),
+      minHeight: (constraints.minHeight + baseSizeAdjustment.dy).clamp(0.0, double.infinity).toDouble(),
+    );
+  }
+
   @override
   bool operator ==(Object other) {
     if (other.runtimeType != runtimeType) {
diff --git a/packages/flutter/test/material/icon_button_test.dart b/packages/flutter/test/material/icon_button_test.dart
index a93f923..094c950 100644
--- a/packages/flutter/test/material/icon_button_test.dart
+++ b/packages/flutter/test/material/icon_button_test.dart
@@ -75,6 +75,69 @@
     expect(iconButton.size, const Size(70.0, 70.0));
   });
 
+  testWidgets('Small icons with non-null constraints can be <48dp', (WidgetTester tester) async {
+    await tester.pumpWidget(
+      wrap(
+        child: IconButton(
+          iconSize: 10.0,
+          onPressed: mockOnPressedFunction,
+          icon: const Icon(Icons.link),
+          constraints: const BoxConstraints(),
+        ),
+      ),
+    );
+
+    final RenderBox iconButton = tester.renderObject(find.byType(IconButton));
+
+    // By default IconButton has a padding of 8.0 on all sides, so both
+    // width and height are 10.0 + 2 * 8.0 = 26.0
+    expect(iconButton.size, const Size(26.0, 26.0));
+  });
+
+  testWidgets('Small icons with non-null constraints and custom padding can be <48dp', (WidgetTester tester) async {
+    await tester.pumpWidget(
+      wrap(
+        child: IconButton(
+          iconSize: 10.0,
+          padding: const EdgeInsets.all(3.0),
+          onPressed: mockOnPressedFunction,
+          icon: const Icon(Icons.link),
+          constraints: const BoxConstraints(),
+        ),
+      ),
+    );
+
+    final RenderBox iconButton = tester.renderObject(find.byType(IconButton));
+
+    // This IconButton has a padding of 3.0 on all sides, so both
+    // width and height are 10.0 + 2 * 3.0 = 16.0
+    expect(iconButton.size, const Size(16.0, 16.0));
+  });
+
+  testWidgets('Small icons comply with VisualDensity requirements', (WidgetTester tester) async {
+    await tester.pumpWidget(
+      wrap(
+        child: Theme(
+          data: ThemeData(visualDensity: const VisualDensity(horizontal: 1, vertical: -1)),
+          child: IconButton(
+            iconSize: 10.0,
+            onPressed: mockOnPressedFunction,
+            icon: const Icon(Icons.link),
+            constraints: const BoxConstraints(minWidth: 32.0, minHeight: 32.0),
+          ),
+        ),
+      ),
+    );
+
+    final RenderBox iconButton = tester.renderObject(find.byType(IconButton));
+
+    // VisualDensity(horizontal: 1, vertical: -1) increases the icon's
+    // width by 4 pixels and decreases its height by 4 pixels, giving
+    // final width 32.0 + 4.0 = 36.0 and
+    // final height 32.0 - 4.0 = 28.0
+    expect(iconButton.size, const Size(36.0, 28.0));
+  });
+
   testWidgets('test default icon buttons are constrained', (WidgetTester tester) async {
     await tester.pumpWidget(
       wrap(