From aa5db20fbb7bb9f8f0e4998be64baf8dad454f27 Mon Sep 17 00:00:00 2001 From: Chinmoy Date: Tue, 28 Mar 2023 03:55:56 +0530 Subject: [PATCH] Added backgroundColor and disabledBackgroundColor to SnackBarAction. (#118786) --- .../flutter/lib/src/material/snack_bar.dart | 41 +++- .../lib/src/material/snack_bar_theme.dart | 31 ++- .../flutter/test/material/snack_bar_test.dart | 201 ++++++++++++++++++ .../test/material/snack_bar_theme_test.dart | 181 +++++++++++++++- 4 files changed, 441 insertions(+), 13 deletions(-) diff --git a/packages/flutter/lib/src/material/snack_bar.dart b/packages/flutter/lib/src/material/snack_bar.dart index 95a808c9c0..0b3f5b1f6b 100644 --- a/packages/flutter/lib/src/material/snack_bar.dart +++ b/packages/flutter/lib/src/material/snack_bar.dart @@ -7,6 +7,7 @@ import 'package:flutter/widgets.dart'; import 'button_style.dart'; import 'color_scheme.dart'; +import 'colors.dart'; import 'icon_button.dart'; import 'icons.dart'; import 'material.dart'; @@ -88,9 +89,13 @@ class SnackBarAction extends StatefulWidget { super.key, this.textColor, this.disabledTextColor, + this.backgroundColor, + this.disabledBackgroundColor, required this.label, required this.onPressed, - }); + }) : assert(backgroundColor is! MaterialStateColor || disabledBackgroundColor == null, + 'disabledBackgroundColor must not be provided when background color is ' + 'a MaterialStateColor'); /// The button label color. If not provided, defaults to /// [SnackBarThemeData.actionTextColor]. @@ -101,10 +106,24 @@ class SnackBarAction extends StatefulWidget { /// hovered and others. final Color? textColor; + /// The button background fill color. If not provided, defaults to + /// [SnackBarThemeData.actionBackgroundColor]. + /// + /// If [backgroundColor] is a [MaterialStateColor], then the text color will + /// be resolved against the set of [MaterialState]s that the action text is + /// in, thus allowing for different colors for the states. + final Color? backgroundColor; + /// The button disabled label color. This color is shown after the /// [SnackBarAction] is dismissed. final Color? disabledTextColor; + /// The button diabled background color. This color is shown after the + /// [SnackBarAction] is dismissed. + /// + /// If not provided, defaults to [SnackBarThemeData.disabledActionBackgroundColor]. + final Color? disabledBackgroundColor; + /// The button label. final String label; @@ -166,9 +185,29 @@ class _SnackBarActionState extends State { }); } + MaterialStateColor? resolveBackgroundColor() { + if (widget.backgroundColor is MaterialStateColor) { + return widget.backgroundColor! as MaterialStateColor; + } + if (snackBarTheme.actionBackgroundColor is MaterialStateColor) { + return snackBarTheme.actionBackgroundColor! as MaterialStateColor; + } + return MaterialStateColor.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return widget.disabledBackgroundColor ?? + snackBarTheme.disabledActionBackgroundColor ?? + Colors.transparent; + } + return widget.backgroundColor ?? + snackBarTheme.actionBackgroundColor ?? + Colors.transparent; + }); + } + return TextButton( style: ButtonStyle( foregroundColor: resolveForegroundColor(), + backgroundColor: resolveBackgroundColor(), ), onPressed: _haveTriggeredAction ? null : _handlePressed, child: Text(widget.label), diff --git a/packages/flutter/lib/src/material/snack_bar_theme.dart b/packages/flutter/lib/src/material/snack_bar_theme.dart index 59bab44332..5f976148d7 100644 --- a/packages/flutter/lib/src/material/snack_bar_theme.dart +++ b/packages/flutter/lib/src/material/snack_bar_theme.dart @@ -7,6 +7,7 @@ import 'dart:ui' show lerpDouble; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; +import 'material_state.dart'; import 'theme.dart'; /// Defines where a [SnackBar] should appear within a [Scaffold] and how its @@ -65,11 +66,16 @@ class SnackBarThemeData with Diagnosticable { this.showCloseIcon, this.closeIconColor, this.actionOverflowThreshold, + this.actionBackgroundColor, + this.disabledActionBackgroundColor }) : assert(elevation == null || elevation >= 0.0), assert(width == null || identical(behavior, SnackBarBehavior.floating), 'Width can only be set if behaviour is SnackBarBehavior.floating'), assert(actionOverflowThreshold == null || (actionOverflowThreshold >= 0 && actionOverflowThreshold <= 1), - 'Action overflow threshold must be between 0 and 1 inclusive'); + 'Action overflow threshold must be between 0 and 1 inclusive'), + assert(actionBackgroundColor is! MaterialStateColor || disabledActionBackgroundColor == null, + 'disabledBackgroundColor must not be provided when background color is ' + 'a MaterialStateColor'); /// Overrides the default value for [SnackBar.backgroundColor]. /// @@ -139,6 +145,15 @@ class SnackBarThemeData with Diagnosticable { /// /// Must be a value between 0 and 1, if present. final double? actionOverflowThreshold; + /// Overrides default value for [SnackBarAction.backgroundColor]. + /// + /// If null, [SnackBarAction] falls back to [Colors.transparent]. + final Color? actionBackgroundColor; + + /// Overrides default value for [SnackBarAction.]. + /// + /// If null, [SnackBarAction] falls back to [Colors.transparent]. + final Color? disabledActionBackgroundColor; /// Creates a copy of this object with the given fields replaced with the /// new values. @@ -155,6 +170,8 @@ class SnackBarThemeData with Diagnosticable { bool? showCloseIcon, Color? closeIconColor, double? actionOverflowThreshold, + Color? actionBackgroundColor, + Color? disabledActionBackgroundColor, }) { return SnackBarThemeData( backgroundColor: backgroundColor ?? this.backgroundColor, @@ -169,6 +186,8 @@ class SnackBarThemeData with Diagnosticable { showCloseIcon: showCloseIcon ?? this.showCloseIcon, closeIconColor: closeIconColor ?? this.closeIconColor, actionOverflowThreshold: actionOverflowThreshold ?? this.actionOverflowThreshold, + actionBackgroundColor: actionBackgroundColor ?? this.actionBackgroundColor, + disabledActionBackgroundColor: disabledActionBackgroundColor ?? this.disabledActionBackgroundColor, ); } @@ -193,6 +212,8 @@ class SnackBarThemeData with Diagnosticable { insetPadding: EdgeInsets.lerp(a?.insetPadding, b?.insetPadding, t), closeIconColor: Color.lerp(a?.closeIconColor, b?.closeIconColor, t), actionOverflowThreshold: lerpDouble(a?.actionOverflowThreshold, b?.actionOverflowThreshold, t), + actionBackgroundColor: Color.lerp(a?.actionBackgroundColor, b?.actionBackgroundColor, t), + disabledActionBackgroundColor: Color.lerp(a?.disabledActionBackgroundColor, b?.disabledActionBackgroundColor, t), ); } @@ -210,6 +231,8 @@ class SnackBarThemeData with Diagnosticable { showCloseIcon, closeIconColor, actionOverflowThreshold, + actionBackgroundColor, + disabledActionBackgroundColor ); @override @@ -232,7 +255,9 @@ class SnackBarThemeData with Diagnosticable { && other.insetPadding == insetPadding && other.showCloseIcon == showCloseIcon && other.closeIconColor == closeIconColor - && other.actionOverflowThreshold == actionOverflowThreshold; + && other.actionOverflowThreshold == actionOverflowThreshold + && other.actionBackgroundColor == actionBackgroundColor + && other.disabledActionBackgroundColor == disabledActionBackgroundColor; } @override @@ -250,5 +275,7 @@ class SnackBarThemeData with Diagnosticable { properties.add(DiagnosticsProperty('showCloseIcon', showCloseIcon, defaultValue: null)); properties.add(ColorProperty('closeIconColor', closeIconColor, defaultValue: null)); properties.add(DoubleProperty('actionOverflowThreshold', actionOverflowThreshold, defaultValue: null)); + properties.add(ColorProperty('actionBackgroundColor', actionBackgroundColor, defaultValue: null)); + properties.add(ColorProperty('disabledActionBackgroundColor', disabledActionBackgroundColor, defaultValue: null)); } } diff --git a/packages/flutter/test/material/snack_bar_test.dart b/packages/flutter/test/material/snack_bar_test.dart index 83b7860e66..621f9b66b7 100644 --- a/packages/flutter/test/material/snack_bar_test.dart +++ b/packages/flutter/test/material/snack_bar_test.dart @@ -2656,6 +2656,207 @@ void main() { ), ); }); + +testWidgets('SnackBarAction backgroundColor works as a Color', (WidgetTester tester) async { + const Color backgroundColor = Colors.blue; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (BuildContext context) { + return GestureDetector( + onTap: () { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('I am a snack bar.'), + duration: const Duration(seconds: 2), + action: SnackBarAction( + backgroundColor: backgroundColor, + label: 'ACTION', + onPressed: () {}, + ), + ), + ); + }, + child: const Text('Tap'), + ); + }, + ), + ), + ), + ); + + await tester.tap(find.text('Tap')); + await tester.pumpAndSettle(); + + final Material materialBeforeDismissed = tester.widget(find.descendant( + of: find.widgetWithText(TextButton, 'ACTION'), + matching: find.byType(Material), + )); + expect(materialBeforeDismissed.color, backgroundColor); + + await tester.tap(find.text('ACTION')); + await tester.pump(); + + final Material materialAfterDismissed = tester.widget(find.descendant( + of: find.widgetWithText(TextButton, 'ACTION'), + matching: find.byType(Material), + )); + expect(materialAfterDismissed.color, Colors.transparent); + }); + + testWidgets('SnackBarAction backgroundColor works as a MaterialStateColor', (WidgetTester tester) async { + final MaterialStateColor backgroundColor = MaterialStateColor.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return Colors.blue; + } + return Colors.purple; + }); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (BuildContext context) { + return GestureDetector( + onTap: () { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('I am a snack bar.'), + duration: const Duration(seconds: 2), + action: SnackBarAction( + backgroundColor: backgroundColor, + label: 'ACTION', + onPressed: () {}, + ), + ), + ); + }, + child: const Text('Tap'), + ); + }, + ), + ), + ), + ); + + await tester.tap(find.text('Tap')); + await tester.pumpAndSettle(); + + final Material materialBeforeDismissed = tester.widget(find.descendant( + of: find.widgetWithText(TextButton, 'ACTION'), + matching: find.byType(Material), + )); + expect(materialBeforeDismissed.color, Colors.purple); + + await tester.tap(find.text('ACTION')); + await tester.pump(); + + final Material materialAfterDismissed = tester.widget(find.descendant( + of: find.widgetWithText(TextButton, 'ACTION'), + matching: find.byType(Material), + )); + expect(materialAfterDismissed.color, Colors.blue); + }); + + testWidgets('SnackBarAction disabledBackgroundColor works as expected', (WidgetTester tester) async { + const Color backgroundColor = Colors.blue; + const Color disabledBackgroundColor = Colors.red; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (BuildContext context) { + return GestureDetector( + onTap: () { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('I am a snack bar.'), + duration: const Duration(seconds: 2), + action: SnackBarAction( + backgroundColor: backgroundColor, + disabledBackgroundColor: disabledBackgroundColor, + label: 'ACTION', + onPressed: () {}, + ), + ), + ); + }, + child: const Text('Tap'), + ); + }, + ), + ), + ), + ); + + await tester.tap(find.text('Tap')); + await tester.pumpAndSettle(); + + final Material materialBeforeDismissed = tester.widget(find.descendant( + of: find.widgetWithText(TextButton, 'ACTION'), + matching: find.byType(Material), + )); + expect(materialBeforeDismissed.color, backgroundColor); + + await tester.tap(find.text('ACTION')); + await tester.pump(); + + final Material materialAfterDismissed = tester.widget(find.descendant( + of: find.widgetWithText(TextButton, 'ACTION'), + matching: find.byType(Material), + )); + expect(materialAfterDismissed.color, disabledBackgroundColor); + }); + + testWidgets('SnackBarAction asserts when backgroundColor is a MaterialStateColor and disabledBackgroundColor is also provided', (WidgetTester tester) async { + final Color backgroundColor = MaterialStateColor.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return Colors.blue; + } + return Colors.purple; + }); + const Color disabledBackgroundColor = Colors.red; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Builder( + builder: (BuildContext context) { + return GestureDetector( + onTap: () { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('I am a snack bar.'), + duration: const Duration(seconds: 2), + action: SnackBarAction( + backgroundColor: backgroundColor, + disabledBackgroundColor: disabledBackgroundColor, + label: 'ACTION', + onPressed: () {}, + ), + ), + ); + }, + child: const Text('Tap'), + ); + }, + ), + ), + ), + ); + + await tester.tap(find.text('Tap')); + await tester.pumpAndSettle(); + + expect(tester.takeException(), isAssertionError.having( + (AssertionError e) => e.toString(), + 'description', + contains('disabledBackgroundColor must not be provided when background color is a MaterialStateColor')) + ); + }); } /// Start test for "SnackBar dismiss test". diff --git a/packages/flutter/test/material/snack_bar_theme_test.dart b/packages/flutter/test/material/snack_bar_theme_test.dart index b8445474f9..d7a266c919 100644 --- a/packages/flutter/test/material/snack_bar_theme_test.dart +++ b/packages/flutter/test/material/snack_bar_theme_test.dart @@ -118,8 +118,7 @@ void main() { )); await tester.tap(find.text('X')); - await tester.pump(); // start animation - await tester.pump(const Duration(milliseconds: 750)); + await tester.pumpAndSettle(); final Material material = _getSnackBarMaterial(tester); final RenderParagraph content = _getSnackBarTextRenderObject(tester, text); @@ -156,8 +155,7 @@ void main() { )); await tester.tap(find.text('X')); - await tester.pump(); // start animation - await tester.pump(const Duration(milliseconds: 750)); + await tester.pumpAndSettle(); final Material material = _getSnackBarMaterial(tester); final RenderParagraph button = _getSnackBarActionTextRenderObject(tester, action); @@ -213,8 +211,7 @@ void main() { )); await tester.tap(find.text('X')); - await tester.pump(); // start animation - await tester.pump(const Duration(milliseconds: 750)); + await tester.pumpAndSettle(); final Finder materialFinder = _getSnackBarMaterialFinder(tester); final Material material = _getSnackBarMaterial(tester); @@ -233,6 +230,148 @@ void main() { expect(snackBarBottomRight.dx, (800 + snackBarWidth) / 2); // Device width is 800. }); + testWidgets('SnackBarAction uses actionBackgroundColor', (WidgetTester tester) async { + final MaterialStateColor actionBackgroundColor = MaterialStateColor.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return Colors.blue; + } + return Colors.purple; + }); + + await tester.pumpWidget(MaterialApp( + theme: ThemeData(snackBarTheme: _createSnackBarTheme(actionBackgroundColor: actionBackgroundColor)), + home: Scaffold( + body: Builder( + builder: (BuildContext context) { + return GestureDetector( + onTap: () { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: const Text('I am a snack bar.'), + action: SnackBarAction( + label: 'ACTION', + onPressed: () {}, + ), + )); + }, + child: const Text('X'), + ); + }, + ), + ), + )); + + await tester.tap(find.text('X')); + await tester.pumpAndSettle(); + + final Material materialBeforeDismissed = tester.widget(find.descendant( + of: find.widgetWithText(TextButton, 'ACTION'), + matching: find.byType(Material), + )); + expect(materialBeforeDismissed.color, Colors.purple); + + await tester.tap(find.text('ACTION')); + await tester.pump(); + + final Material materialAfterDismissed = tester.widget(find.descendant( + of: find.widgetWithText(TextButton, 'ACTION'), + matching: find.byType(Material), + )); + expect(materialAfterDismissed.color, Colors.blue); + }); + + testWidgets('SnackBarAction backgroundColor overrides SnackBarThemeData actionBackgroundColor', (WidgetTester tester) async { + final MaterialStateColor snackBarActionBackgroundColor = MaterialStateColor.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return Colors.amber; + } + return Colors.cyan; + }); + + final MaterialStateColor actionBackgroundColor = MaterialStateColor.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return Colors.blue; + } + return Colors.purple; + }); + + await tester.pumpWidget(MaterialApp( + theme: ThemeData(snackBarTheme: _createSnackBarTheme(actionBackgroundColor: actionBackgroundColor)), + home: Scaffold( + body: Builder( + builder: (BuildContext context) { + return GestureDetector( + onTap: () { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: const Text('I am a snack bar.'), + action: SnackBarAction( + label: 'ACTION', + backgroundColor: snackBarActionBackgroundColor, + onPressed: () {}, + ), + )); + }, + child: const Text('X'), + ); + }, + ), + ), + )); + + await tester.tap(find.text('X')); + await tester.pumpAndSettle(); + + final Material materialBeforeDismissed = tester.widget(find.descendant( + of: find.widgetWithText(TextButton, 'ACTION'), + matching: find.byType(Material), + )); + expect(materialBeforeDismissed.color, Colors.cyan); + + await tester.tap(find.text('ACTION')); + await tester.pump(); + + final Material materialAfterDismissed = tester.widget(find.descendant( + of: find.widgetWithText(TextButton, 'ACTION'), + matching: find.byType(Material), + )); + expect(materialAfterDismissed.color, Colors.amber); + }); + + testWidgets('SnackBarThemeData asserts when actionBackgroundColor is a MaterialStateColor and disabledActionBackgroundColor is also provided', (WidgetTester tester) async { + final MaterialStateColor actionBackgroundColor = MaterialStateColor.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return Colors.blue; + } + return Colors.purple; + }); + + expect(() => tester.pumpWidget(MaterialApp( + theme: ThemeData(snackBarTheme: _createSnackBarTheme(actionBackgroundColor: actionBackgroundColor, disabledActionBackgroundColor: Colors.amber)), + home: Scaffold( + body: Builder( + builder: (BuildContext context) { + return GestureDetector( + onTap: () { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: const Text('I am a snack bar.'), + action: SnackBarAction( + label: 'ACTION', + onPressed: () {}, + ), + )); + }, + child: const Text('X'), + ); + }, + ), + ), + )), throwsA(isA().having( + (AssertionError e) => e.toString(), + 'description', + contains('disabledBackgroundColor must not be provided when background color is a MaterialStateColor')) + ) + ); + }); + testWidgets('SnackBar theme behavior is correct for floating', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( theme: ThemeData( @@ -260,8 +399,7 @@ void main() { )); await tester.tap(find.text('X')); - await tester.pump(); // start animation - await tester.pump(const Duration(milliseconds: 750)); + await tester.pumpAndSettle(); final RenderBox snackBarBox = tester.firstRenderObject(find.byType(SnackBar)); final RenderBox floatingActionButtonBox = tester.firstRenderObject(find.byType(FloatingActionButton)); @@ -305,8 +443,7 @@ void main() { final Offset floatingActionButtonOriginBottomCenter = floatingActionButtonOriginBox.localToGlobal(floatingActionButtonOriginBox.size.bottomCenter(Offset.zero)); await tester.tap(find.text('X')); - await tester.pump(); // start animation - await tester.pump(const Duration(milliseconds: 750)); + await tester.pumpAndSettle(); final RenderBox snackBarBox = tester.firstRenderObject(find.byType(SnackBar)); final RenderBox floatingActionButtonBox = tester.firstRenderObject(find.byType(FloatingActionButton)); @@ -435,6 +572,30 @@ SnackBarThemeData _snackBarTheme({bool? showCloseIcon}) { ); } +SnackBarThemeData _createSnackBarTheme({ + Color? backgroundColor, + Color? actionTextColor, + Color? disabledActionTextColor, + TextStyle? contentTextStyle, + double? elevation, + ShapeBorder? shape, + SnackBarBehavior? behavior, + Color? actionBackgroundColor, + Color? disabledActionBackgroundColor +}) { + return SnackBarThemeData( + backgroundColor: backgroundColor, + actionTextColor: actionTextColor, + disabledActionTextColor: disabledActionTextColor, + contentTextStyle: contentTextStyle, + elevation: elevation, + shape: shape, + behavior: behavior, + actionBackgroundColor: actionBackgroundColor, + disabledActionBackgroundColor: disabledActionBackgroundColor + ); +} + Material _getSnackBarMaterial(WidgetTester tester) { return tester.widget( _getSnackBarMaterialFinder(tester).first,