Add width property to SnackBarThemeData (#112636)

* Adding snackbar theme data width field

* Whitespace formatting

* Update docstrings

* version update

* tidy up

* Revert auto text formatting

* Text formatting

* Remove whitespace

* Test tidy

* Whitespace fix

* y Please enter the commit message for your changes. Lines starting

* whitespace

* test fixes

* de-British-ification

* comment modification
diff --git a/packages/flutter/lib/src/material/scaffold.dart b/packages/flutter/lib/src/material/scaffold.dart
index e7aae7d..07e69bb 100644
--- a/packages/flutter/lib/src/material/scaffold.dart
+++ b/packages/flutter/lib/src/material/scaffold.dart
@@ -2832,7 +2832,7 @@
         ?? themeData.snackBarTheme.behavior
         ?? SnackBarBehavior.fixed;
       isSnackBarFloating = snackBarBehavior == SnackBarBehavior.floating;
-      snackBarWidth = _messengerSnackBar?._widget.width;
+      snackBarWidth = _messengerSnackBar?._widget.width ?? themeData.snackBarTheme.width;
 
       _addIfNonNull(
         children,
diff --git a/packages/flutter/lib/src/material/snack_bar.dart b/packages/flutter/lib/src/material/snack_bar.dart
index d4a2382..e56fc85 100644
--- a/packages/flutter/lib/src/material/snack_bar.dart
+++ b/packages/flutter/lib/src/material/snack_bar.dart
@@ -293,8 +293,9 @@
   /// available space. This property is only used when [behavior] is
   /// [SnackBarBehavior.floating]. It can not be used if [margin] is specified.
   ///
-  /// If this property is null, then the snack bar will take up the full device
-  /// width less the margin.
+  /// If this property is null, then [SnackBarThemeData.width] of
+  /// [ThemeData.snackBarTheme] is used. If that is null, the snack bar will
+  /// take up the full device width less the margin.
   final double? width;
 
   /// The shape of the snack bar's [Material].
@@ -470,6 +471,7 @@
 
     final TextStyle? contentTextStyle = snackBarTheme.contentTextStyle ?? ThemeData(brightness: brightness).textTheme.titleMedium;
     final SnackBarBehavior snackBarBehavior = widget.behavior ?? snackBarTheme.behavior ?? SnackBarBehavior.fixed;
+    final double? width = widget.width ?? snackBarTheme.width;
     assert((){
       // Whether the behavior is set through the constructor or the theme,
       // assert that our other properties are configured properly.
@@ -485,7 +487,7 @@
           }
         }
         assert(widget.margin == null, message('Margin'));
-        assert(widget.width == null, message('Width'));
+        assert(width == null, message('Width'));
       }
       return true;
     }());
@@ -567,10 +569,10 @@
       const double topMargin = 5.0;
       const double bottomMargin = 10.0;
       // If width is provided, do not include horizontal margins.
-      if (widget.width != null) {
+      if (width != null) {
         snackBar = Container(
           margin: const EdgeInsets.only(top: topMargin, bottom: bottomMargin),
-          width: widget.width,
+          width: width,
           child: snackBar,
         );
       } else {
diff --git a/packages/flutter/lib/src/material/snack_bar_theme.dart b/packages/flutter/lib/src/material/snack_bar_theme.dart
index 33ab1fa..4e46378 100644
--- a/packages/flutter/lib/src/material/snack_bar_theme.dart
+++ b/packages/flutter/lib/src/material/snack_bar_theme.dart
@@ -60,8 +60,12 @@
     this.elevation,
     this.shape,
     this.behavior,
-  }) : assert(elevation == null || elevation >= 0.0);
-
+    this.width,
+  })  : assert(elevation == null || elevation >= 0.0),
+        assert(
+            width == null ||
+                (width != null && identical(behavior, SnackBarBehavior.floating)),
+            'Width can only be set if behaviour is SnackBarBehavior.floating');
   /// Default value for [SnackBar.backgroundColor].
   ///
   /// If null, [SnackBar] defaults to dark grey: `Color(0xFF323232)`.
@@ -104,6 +108,13 @@
   /// If null, [SnackBar] will default to [SnackBarBehavior.fixed].
   final SnackBarBehavior? behavior;
 
+  /// Default value for [SnackBar.width].
+  ///
+  /// If this property is null, then the snack bar will take up the full device
+  /// width less the margin. This value is only used when [behavior] is
+  /// [SnackBarBehavior.floating].
+  final double? width;
+
   /// Creates a copy of this object with the given fields replaced with the
   /// new values.
   SnackBarThemeData copyWith({
@@ -114,6 +125,7 @@
     double? elevation,
     ShapeBorder? shape,
     SnackBarBehavior? behavior,
+    double? width,
   }) {
     return SnackBarThemeData(
       backgroundColor: backgroundColor ?? this.backgroundColor,
@@ -123,6 +135,7 @@
       elevation: elevation ?? this.elevation,
       shape: shape ?? this.shape,
       behavior: behavior ?? this.behavior,
+      width: width ?? this.width,
     );
   }
 
@@ -141,19 +154,21 @@
       elevation: lerpDouble(a?.elevation, b?.elevation, t),
       shape: ShapeBorder.lerp(a?.shape, b?.shape, t),
       behavior: t < 0.5 ? a?.behavior : b?.behavior,
+      width: lerpDouble(a?.width, b?.width, t),
     );
   }
 
   @override
   int get hashCode => Object.hash(
-    backgroundColor,
-    actionTextColor,
-    disabledActionTextColor,
-    contentTextStyle,
-    elevation,
-    shape,
-    behavior,
-  );
+        backgroundColor,
+        actionTextColor,
+        disabledActionTextColor,
+        contentTextStyle,
+        elevation,
+        shape,
+        behavior,
+        width,
+      );
 
   @override
   bool operator ==(Object other) {
@@ -170,7 +185,8 @@
         && other.contentTextStyle == contentTextStyle
         && other.elevation == elevation
         && other.shape == shape
-        && other.behavior == behavior;
+        && other.behavior == behavior
+        && other.width == width;
   }
 
   @override
@@ -183,5 +199,6 @@
     properties.add(DoubleProperty('elevation', elevation, defaultValue: null));
     properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: null));
     properties.add(DiagnosticsProperty<SnackBarBehavior>('behavior', behavior, defaultValue: null));
+    properties.add(DoubleProperty('width', width, defaultValue: null));
   }
 }
diff --git a/packages/flutter/test/material/snack_bar_test.dart b/packages/flutter/test/material/snack_bar_test.dart
index 46e9db1..53a820a 100644
--- a/packages/flutter/test/material/snack_bar_test.dart
+++ b/packages/flutter/test/material/snack_bar_test.dart
@@ -724,6 +724,93 @@
     expect(snackBarBottomRight.dx, (800 + width) / 2); // Device width is 800.
   });
 
+  testWidgets('Snackbar width can be customized from ThemeData',
+      (WidgetTester tester) async {
+    const double width = 200.0;
+    await tester.pumpWidget(
+      MaterialApp(
+        theme: ThemeData(
+          snackBarTheme: const SnackBarThemeData(
+              width: width, behavior: SnackBarBehavior.floating),
+        ),
+        home: Scaffold(
+          body: Builder(
+            builder: (BuildContext context) {
+              return GestureDetector(
+                onTap: () {
+                  ScaffoldMessenger.of(context).showSnackBar(
+                    const SnackBar(
+                      content: Text('Feeling snackish'),
+                    ),
+                  );
+                },
+                child: const Text('X'),
+              );
+            },
+          ),
+        ),
+      ),
+    );
+
+    await tester.tap(find.text('X'));
+    await tester.pump(); // start animation
+    await tester.pump(const Duration(milliseconds: 750));
+
+    final Finder materialFinder = find.descendant(
+      of: find.byType(SnackBar),
+      matching: find.byType(Material),
+    );
+    final Offset snackBarBottomLeft = tester.getBottomLeft(materialFinder);
+    final Offset snackBarBottomRight = tester.getBottomRight(materialFinder);
+    expect(snackBarBottomLeft.dx, (800 - width) / 2); // Device width is 800.
+    expect(snackBarBottomRight.dx, (800 + width) / 2); // Device width is 800.
+  });
+
+  testWidgets(
+      'Snackbar width customization takes preference of widget over theme',
+      (WidgetTester tester) async {
+    const double themeWidth = 200.0;
+    const double widgetWidth = 400.0;
+    await tester.pumpWidget(
+      MaterialApp(
+        theme: ThemeData(
+          snackBarTheme: const SnackBarThemeData(
+              width: themeWidth, behavior: SnackBarBehavior.floating),
+        ),
+        home: Scaffold(
+          body: Builder(
+            builder: (BuildContext context) {
+              return GestureDetector(
+                onTap: () {
+                  ScaffoldMessenger.of(context).showSnackBar(
+                    const SnackBar(
+                      content: Text('Feeling super snackish'),
+                      width: widgetWidth,
+                    ),
+                  );
+                },
+                child: const Text('X'),
+              );
+            },
+          ),
+        ),
+      ),
+    );
+
+    await tester.tap(find.text('X'));
+    await tester.pump(); // start animation
+    await tester.pump(const Duration(milliseconds: 750));
+
+    final Finder materialFinder = find.descendant(
+      of: find.byType(SnackBar),
+      matching: find.byType(Material),
+    );
+    final Offset snackBarBottomLeft = tester.getBottomLeft(materialFinder);
+    final Offset snackBarBottomRight = tester.getBottomRight(materialFinder);
+    expect(snackBarBottomLeft.dx, (800 - widgetWidth) / 2); // Device width is 800.
+    expect(snackBarBottomRight.dx, (800 + widgetWidth) / 2); // Device width is 800.
+  });
+
   testWidgets('Snackbar labels can be colored', (WidgetTester tester) async {
     await tester.pumpWidget(
       MaterialApp(
diff --git a/packages/flutter/test/material/snack_bar_theme_test.dart b/packages/flutter/test/material/snack_bar_theme_test.dart
index 240e27b..7162b5d 100644
--- a/packages/flutter/test/material/snack_bar_theme_test.dart
+++ b/packages/flutter/test/material/snack_bar_theme_test.dart
@@ -21,9 +21,22 @@
     expect(snackBarTheme.elevation, null);
     expect(snackBarTheme.shape, null);
     expect(snackBarTheme.behavior, null);
+    expect(snackBarTheme.width, null);
   });
 
-  testWidgets('Default SnackBarThemeData debugFillProperties', (WidgetTester tester) async {
+  test(
+      'SnackBarTheme throws assertion if width is provided with fixed behaviour',
+      () {
+    expect(
+        () => SnackBarThemeData(
+              behavior: SnackBarBehavior.fixed,
+              width: 300.0,
+            ),
+        throwsAssertionError);
+  });
+
+  testWidgets('Default SnackBarThemeData debugFillProperties',
+      (WidgetTester tester) async {
     final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
     const SnackBarThemeData().debugFillProperties(builder);
 
@@ -45,6 +58,7 @@
       elevation: 2.0,
       shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(2.0))),
       behavior: SnackBarBehavior.floating,
+      width: 400.0,
     ).debugFillProperties(builder);
 
     final List<String> description = builder.properties
@@ -60,6 +74,7 @@
       'elevation: 2.0',
       'shape: RoundedRectangleBorder(BorderSide(width: 0.0, style: none), BorderRadius.circular(2.0))',
       'behavior: SnackBarBehavior.floating',
+      'width: 400.0',
     ]);
   });
 
@@ -145,6 +160,7 @@
     const ShapeBorder shape = RoundedRectangleBorder(
       borderRadius: BorderRadius.all(Radius.circular(9.0)),
     );
+    const double snackBarWidth = 400.0;
 
     await tester.pumpWidget(MaterialApp(
       theme: ThemeData(snackBarTheme: _snackBarTheme()),
@@ -155,6 +171,8 @@
               onTap: () {
                 ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                   backgroundColor: backgroundColor,
+                  behavior: SnackBarBehavior.floating,
+                  width: snackBarWidth,
                   elevation: elevation,
                   shape: shape,
                   content: const Text('I am a snack bar.'),
@@ -177,13 +195,20 @@
     await tester.pump(); // start animation
     await tester.pump(const Duration(milliseconds: 750));
 
+    final Finder materialFinder = _getSnackBarMaterialFinder(tester);
     final Material material = _getSnackBarMaterial(tester);
-    final RenderParagraph button = _getSnackBarActionTextRenderObject(tester, action);
+    final RenderParagraph button =
+        _getSnackBarActionTextRenderObject(tester, action);
 
     expect(material.color, backgroundColor);
     expect(material.elevation, elevation);
     expect(material.shape, shape);
     expect(button.text.style!.color, textColor);
+    // Assert width.
+    final Offset snackBarBottomLeft = tester.getBottomLeft(materialFinder.first);
+    final Offset snackBarBottomRight = tester.getBottomRight(materialFinder.first);
+    expect(snackBarBottomLeft.dx, (800 - snackBarWidth) / 2); // Device width is 800.
+    expect(snackBarBottomRight.dx, (800 + snackBarWidth) / 2); // Device width is 800.
   });
 
   testWidgets('SnackBar theme behavior is correct for floating', (WidgetTester tester) async {
@@ -376,10 +401,15 @@
 
 Material _getSnackBarMaterial(WidgetTester tester) {
   return tester.widget<Material>(
-    find.descendant(
-      of: find.byType(SnackBar),
-      matching: find.byType(Material),
-    ).first,
+    _getSnackBarMaterialFinder(tester).first,
+  );
+}
+
+Finder _getSnackBarMaterialFinder(WidgetTester tester) {
+  return find.descendant(
+    of: find.byType(SnackBar),
+    matching: find.byType(Material),
+
   );
 }