From 467c970bfb055af624ebba4771619defa74bb177 Mon Sep 17 00:00:00 2001 From: Taha Tesser Date: Tue, 20 Jun 2023 01:03:26 +0300 Subject: [PATCH] Introduce MaterialState `color` property for chips (#128584) fixes https://github.com/flutter/flutter/issues/115827 fixes https://github.com/flutter/flutter/issues/101325 ### Description 1. This PR adds a new MaterialState `color` property to all the chips (this makes it possible to customize chips in all states from the M3 specs). 2. Updated defaults to use the new MaterialState `color` property. 3. Updated and added new tests to all the chip test classes.
code sample ```dart import 'package:flutter/material.dart'; const Color disabledColor = Colors.black26; const Color backgroundColor = Colors.cyan; final Color disabledSelectedColor = Colors.red.shade100; const Color selectedColor = Colors.amber; final MaterialStateProperty color = MaterialStateProperty.resolveWith((Set states) { if (states.contains(MaterialState.disabled) && states.contains(MaterialState.selected)) { return disabledSelectedColor; } if (states.contains(MaterialState.disabled)) { return disabledColor; } if (states.contains(MaterialState.selected)) { return selectedColor; } return backgroundColor; }); void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, theme: ThemeData( useMaterial3: true, // chipTheme: ChipThemeData(color: color), ), home: const Example(), ); } } class Example extends StatefulWidget { const Example({super.key}); @override State createState() => _ExampleState(); } class _ExampleState extends State { bool enabled = false; bool selected = true; @override Widget build(BuildContext context) { const Widget verticalSpace = SizedBox(height: 20); return Scaffold( body: Center( child: Column( children: [ const SizedBox(height: 25), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ const Card( elevation: 0.0, color: disabledColor, child: Padding( padding: EdgeInsets.all(8.0), child: Text('disabledColor'), ), ), const Card( elevation: 0.0, color: backgroundColor, child: Padding( padding: EdgeInsets.all(8.0), child: Text('backgroundColor'), ), ), Card( elevation: 0.0, color: disabledSelectedColor, child: const Padding( padding: EdgeInsets.all(8.0), child: Text('disabledSelectedColor'), ), ), const Card( elevation: 0.0, color: selectedColor, child: Padding( padding: EdgeInsets.all(8.0), child: Text('selectedColor'), ), ), ], ), const Spacer(), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ RawChip( selected: selected, selectedColor: selectedColor, color: color, label: const Text('RawChip'), isEnabled: enabled, onSelected: enabled ? (bool value) {} : null, ), verticalSpace, InputChip( isEnabled: enabled, selected: selected, selectedColor: selectedColor, color: color, label: const Text('InputChip'), onSelected: enabled ? (bool value) {} : null, ), ], ), Column( mainAxisSize: MainAxisSize.min, children: [ FilterChip( selected: selected, selectedColor: selectedColor, color: color, label: const Text('FilterChip'), onSelected: enabled ? (bool value) {} : null, ), verticalSpace, FilterChip.elevated( selected: selected, selectedColor: selectedColor, color: color, label: const Text('FilterChip.elevated'), onSelected: enabled ? (bool value) {} : null, ), ], ), Column( mainAxisSize: MainAxisSize.min, children: [ ChoiceChip( selected: selected, selectedColor: selectedColor, color: color, label: const Text('ChoiceChip'), onSelected: enabled ? (bool value) {} : null, ), verticalSpace, ChoiceChip.elevated( selected: selected, selectedColor: selectedColor, color: color, label: const Text('ChoiceChip.elevated'), onSelected: enabled ? (bool value) {} : null, ), ], ), ], ), const Spacer(), Row( children: [ Flexible( child: SwitchListTile( title: const Text('Enabled'), value: enabled, onChanged: (bool value) { setState(() => enabled = value); }, ), ), Flexible( child: SwitchListTile( title: const Text('Selected'), value: selected, onChanged: (bool value) { setState(() => selected = value); }, ), ), ], ) ], ), ), ); } } ```
### Before (not possible to customize disabled and selected chips) ![Screenshot 2023-06-13 at 16 27 13](https://github.com/flutter/flutter/assets/48603081/633f09f7-16a1-469e-b326-b9cc0ed59242) ### After (using disabled and selected chips using the new MaterialState `color` property) ![Screenshot 2023-06-13 at 16 26 53](https://github.com/flutter/flutter/assets/48603081/7f5dffb7-4074-4268-87c0-c059c2da67a8) --- .../lib/action_chip_template.dart | 18 +- dev/tools/gen_defaults/lib/chip_template.dart | 8 +- .../lib/filter_chip_template.dart | 38 +- .../gen_defaults/lib/input_chip_template.dart | 22 +- .../flutter/lib/src/material/action_chip.dart | 24 +- packages/flutter/lib/src/material/chip.dart | 95 ++++- .../flutter/lib/src/material/chip_theme.dart | 14 + .../flutter/lib/src/material/choice_chip.dart | 44 ++- .../flutter/lib/src/material/filter_chip.dart | 44 ++- .../flutter/lib/src/material/input_chip.dart | 27 +- .../test/material/action_chip_test.dart | 127 ++++++- packages/flutter/test/material/chip_test.dart | 335 +++++++++++++++++- .../test/material/chip_theme_test.dart | 145 +++++++- .../test/material/choice_chip_test.dart | 187 +++++++++- .../test/material/filter_chip_test.dart | 184 ++++++++++ .../test/material/input_chip_test.dart | 97 ++++- 16 files changed, 1271 insertions(+), 138 deletions(-) diff --git a/dev/tools/gen_defaults/lib/action_chip_template.dart b/dev/tools/gen_defaults/lib/action_chip_template.dart index 4d6729415e..54027c13d7 100644 --- a/dev/tools/gen_defaults/lib/action_chip_template.dart +++ b/dev/tools/gen_defaults/lib/action_chip_template.dart @@ -41,7 +41,15 @@ class _${blockName}DefaultsM3 extends ChipThemeData { TextStyle? get labelStyle => ${textStyle("$tokenGroup.label-text")}; @override - Color? get backgroundColor => ${componentColor("$tokenGroup$flatVariant.container")}; + MaterialStateProperty? get color => + MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return _chipVariant == _ChipVariant.flat + ? ${componentColor("$tokenGroup$flatVariant.disabled.container")} + : ${componentColor("$tokenGroup$elevatedVariant.disabled.container")}; + } + return ${componentColor("$tokenGroup$flatVariant.container")}; + }); @override Color? get shadowColor => _chipVariant == _ChipVariant.flat @@ -51,17 +59,9 @@ class _${blockName}DefaultsM3 extends ChipThemeData { @override Color? get surfaceTintColor => ${colorOrTransparent("$tokenGroup.container.surface-tint-layer.color")}; - @override - Color? get selectedColor => ${componentColor("$tokenGroup$flatVariant.selected.container")}; - @override Color? get checkmarkColor => ${color("$tokenGroup.with-icon.selected.icon.color")}; - @override - Color? get disabledColor => _chipVariant == _ChipVariant.flat - ? ${componentColor("$tokenGroup$flatVariant.disabled.container")} - : ${componentColor("$tokenGroup$elevatedVariant.disabled.container")}; - @override Color? get deleteIconColor => ${color("$tokenGroup.with-icon.selected.icon.color")}; diff --git a/dev/tools/gen_defaults/lib/chip_template.dart b/dev/tools/gen_defaults/lib/chip_template.dart index 016bd402ec..8d9c05b7cc 100644 --- a/dev/tools/gen_defaults/lib/chip_template.dart +++ b/dev/tools/gen_defaults/lib/chip_template.dart @@ -32,7 +32,7 @@ class _${blockName}DefaultsM3 extends ChipThemeData { TextStyle? get labelStyle => ${textStyle("$tokenGroup.label-text")}; @override - Color? get backgroundColor => ${componentColor("$tokenGroup$variant.container")}; + MaterialStateProperty? get color => null; // Subclasses override this getter @override Color? get shadowColor => ${colorOrTransparent("$tokenGroup.container.shadow-color")}; @@ -40,15 +40,9 @@ class _${blockName}DefaultsM3 extends ChipThemeData { @override Color? get surfaceTintColor => ${colorOrTransparent("$tokenGroup.container.surface-tint-layer.color")}; - @override - Color? get selectedColor => ${componentColor("$tokenGroup$variant.selected.container")}; - @override Color? get checkmarkColor => ${color("$tokenGroup.with-icon.selected.icon.color")}; - @override - Color? get disabledColor => ${componentColor("$tokenGroup$variant.disabled.container")}; - @override Color? get deleteIconColor => ${color("$tokenGroup.with-icon.selected.icon.color")}; diff --git a/dev/tools/gen_defaults/lib/filter_chip_template.dart b/dev/tools/gen_defaults/lib/filter_chip_template.dart index 54119c59b7..6609613cde 100644 --- a/dev/tools/gen_defaults/lib/filter_chip_template.dart +++ b/dev/tools/gen_defaults/lib/filter_chip_template.dart @@ -46,7 +46,27 @@ class _${blockName}DefaultsM3 extends ChipThemeData { TextStyle? get labelStyle => ${textStyle("$tokenGroup.label-text")}; @override - Color? get backgroundColor => ${componentColor("$tokenGroup$flatVariant.container")}; + MaterialStateProperty? get color => + MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.selected) && states.contains(MaterialState.disabled)) { + return _chipVariant == _ChipVariant.flat + ? ${componentColor("$tokenGroup$flatVariant.disabled.selected.container")} + : ${componentColor("$tokenGroup$elevatedVariant.disabled.container")}; + } + if (states.contains(MaterialState.disabled)) { + return _chipVariant == _ChipVariant.flat + ? ${componentColor("$tokenGroup$flatVariant.disabled.unselected.container")} + : ${componentColor("$tokenGroup$elevatedVariant.disabled.container")}; + } + if (states.contains(MaterialState.selected)) { + return _chipVariant == _ChipVariant.flat + ? ${componentColor("$tokenGroup$flatVariant.selected.container")} + : ${componentColor("$tokenGroup$elevatedVariant.selected.container")}; + } + return _chipVariant == _ChipVariant.flat + ? ${componentColor("$tokenGroup$flatVariant.container")} + : ${componentColor("$tokenGroup$elevatedVariant.container")}; + }); @override Color? get shadowColor => _chipVariant == _ChipVariant.flat @@ -56,25 +76,9 @@ class _${blockName}DefaultsM3 extends ChipThemeData { @override Color? get surfaceTintColor => ${colorOrTransparent("$tokenGroup.container.surface-tint-layer.color")}; - @override - Color? get selectedColor => _chipVariant == _ChipVariant.flat - ? isEnabled - ? ${componentColor("$tokenGroup$flatVariant.selected.container")} - : ${componentColor("$tokenGroup$flatVariant.disabled.selected.container")} - : isEnabled - ? ${componentColor("$tokenGroup$elevatedVariant.selected.container")} - : ${componentColor("$tokenGroup$elevatedVariant.disabled.container")}; - @override Color? get checkmarkColor => ${color("$tokenGroup.with-leading-icon.selected.leading-icon.color")}; - @override - Color? get disabledColor => _chipVariant == _ChipVariant.flat - ? isSelected - ? ${componentColor("$tokenGroup$flatVariant.disabled.selected.container")} - : ${componentColor("$tokenGroup$flatVariant.disabled.unselected.container")} - : ${componentColor("$tokenGroup$elevatedVariant.disabled.container")}; - @override Color? get deleteIconColor => ${color("$tokenGroup.with-trailing-icon.selected.trailing-icon.color")}; diff --git a/dev/tools/gen_defaults/lib/input_chip_template.dart b/dev/tools/gen_defaults/lib/input_chip_template.dart index 9226a73da5..245a171b90 100644 --- a/dev/tools/gen_defaults/lib/input_chip_template.dart +++ b/dev/tools/gen_defaults/lib/input_chip_template.dart @@ -33,7 +33,19 @@ class _${blockName}DefaultsM3 extends ChipThemeData { TextStyle? get labelStyle => ${textStyle("$tokenGroup.label-text")}; @override - Color? get backgroundColor => ${componentColor("$tokenGroup$variant.container")}; + MaterialStateProperty? get color => + MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.selected) && states.contains(MaterialState.disabled)) { + return ${componentColor("$tokenGroup$variant.disabled.selected.container")}; + } + if (states.contains(MaterialState.disabled)) { + return ${componentColor("$tokenGroup$variant.disabled.container")}; + } + if (states.contains(MaterialState.selected)) { + return ${componentColor("$tokenGroup$variant.selected.container")}; + } + return ${componentColor("$tokenGroup$variant.container")}; + }); @override Color? get shadowColor => ${colorOrTransparent("$tokenGroup.container.shadow-color")}; @@ -41,17 +53,9 @@ class _${blockName}DefaultsM3 extends ChipThemeData { @override Color? get surfaceTintColor => ${colorOrTransparent("$tokenGroup.container.surface-tint-layer.color")}; - @override - Color? get selectedColor => isEnabled - ? ${componentColor("$tokenGroup$variant.selected.container")} - : ${componentColor("$tokenGroup$variant.disabled.selected.container")}; - @override Color? get checkmarkColor => ${color("$tokenGroup.with-icon.selected.icon.color")}; - @override - Color? get disabledColor => ${componentColor("$tokenGroup$variant.disabled.container")}; - @override Color? get deleteIconColor => ${color("$tokenGroup.with-trailing-icon.selected.trailing-icon.color")}; diff --git a/packages/flutter/lib/src/material/action_chip.dart b/packages/flutter/lib/src/material/action_chip.dart index 826842dead..2d6b330a64 100644 --- a/packages/flutter/lib/src/material/action_chip.dart +++ b/packages/flutter/lib/src/material/action_chip.dart @@ -10,6 +10,7 @@ import 'chip_theme.dart'; import 'color_scheme.dart'; import 'colors.dart'; import 'debug.dart'; +import 'material_state.dart'; import 'text_theme.dart'; import 'theme.dart'; import 'theme_data.dart'; @@ -81,6 +82,7 @@ class ActionChip extends StatelessWidget implements ChipAttributes, TappableChip this.clipBehavior = Clip.none, this.focusNode, this.autofocus = false, + this.color, this.backgroundColor, this.disabledColor, this.padding, @@ -113,6 +115,7 @@ class ActionChip extends StatelessWidget implements ChipAttributes, TappableChip this.clipBehavior = Clip.none, this.focusNode, this.autofocus = false, + this.color, this.backgroundColor, this.disabledColor, this.padding, @@ -151,6 +154,8 @@ class ActionChip extends StatelessWidget implements ChipAttributes, TappableChip @override final bool autofocus; @override + final MaterialStateProperty? color; + @override final Color? backgroundColor; @override final Color? disabledColor; @@ -188,6 +193,7 @@ class ActionChip extends StatelessWidget implements ChipAttributes, TappableChip pressElevation: pressElevation, tooltip: tooltip, labelStyle: labelStyle, + color: color, backgroundColor: backgroundColor, side: side, shape: shape, @@ -239,7 +245,15 @@ class _ActionChipDefaultsM3 extends ChipThemeData { TextStyle? get labelStyle => _textTheme.labelLarge; @override - Color? get backgroundColor => null; + MaterialStateProperty? get color => + MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return _chipVariant == _ChipVariant.flat + ? null + : _colors.onSurface.withOpacity(0.12); + } + return null; + }); @override Color? get shadowColor => _chipVariant == _ChipVariant.flat @@ -249,17 +263,9 @@ class _ActionChipDefaultsM3 extends ChipThemeData { @override Color? get surfaceTintColor => _colors.surfaceTint; - @override - Color? get selectedColor => null; - @override Color? get checkmarkColor => null; - @override - Color? get disabledColor => _chipVariant == _ChipVariant.flat - ? null - : _colors.onSurface.withOpacity(0.12); - @override Color? get deleteIconColor => null; diff --git a/packages/flutter/lib/src/material/chip.dart b/packages/flutter/lib/src/material/chip.dart index d891c9a67d..eb457147e0 100644 --- a/packages/flutter/lib/src/material/chip.dart +++ b/packages/flutter/lib/src/material/chip.dart @@ -137,6 +137,13 @@ abstract interface class ChipAttributes { /// {@macro flutter.widgets.Focus.autofocus} bool get autofocus; + /// The color that fills the chip, in all [MaterialState]s. + /// + /// Resolves in the following states: + /// * [MaterialState.selected]. + /// * [MaterialState.disabled]. + MaterialStateProperty? get color; + /// Color to be used for the unselected, enabled chip's background. /// /// The default is light grey. @@ -561,6 +568,7 @@ class Chip extends StatelessWidget implements ChipAttributes, DeletableChipAttri this.clipBehavior = Clip.none, this.focusNode, this.autofocus = false, + this.color, this.backgroundColor, this.padding, this.visualDensity, @@ -595,6 +603,8 @@ class Chip extends StatelessWidget implements ChipAttributes, DeletableChipAttri @override final bool autofocus; @override + final MaterialStateProperty? color; + @override final Color? backgroundColor; @override final EdgeInsetsGeometry? padding; @@ -644,6 +654,7 @@ class Chip extends StatelessWidget implements ChipAttributes, DeletableChipAttri clipBehavior: clipBehavior, focusNode: focusNode, autofocus: autofocus, + color: color, backgroundColor: backgroundColor, padding: padding, visualDensity: visualDensity, @@ -729,6 +740,7 @@ class RawChip extends StatefulWidget this.clipBehavior = Clip.none, this.focusNode, this.autofocus = false, + this.color, this.backgroundColor, this.materialTapTargetSize, this.elevation, @@ -798,6 +810,8 @@ class RawChip extends StatefulWidget @override final bool autofocus; @override + final MaterialStateProperty? color; + @override final Color? backgroundColor; @override final EdgeInsetsGeometry? padding; @@ -987,23 +1001,47 @@ class _RawChipState extends State with MaterialStateMixin, TickerProvid return resolvedShape.copyWith(side: resolvedSide); } + Color? resolveColor({ + MaterialStateProperty? color, + Color? selectedColor, + Color? backgroundColor, + Color? disabledColor, + MaterialStateProperty? defaultColor, + }) { + return _IndividualOverrides( + color: color, + selectedColor: selectedColor, + backgroundColor: backgroundColor, + disabledColor: disabledColor, + ).resolve(materialStates) ?? defaultColor?.resolve(materialStates); + } + /// Picks between three different colors, depending upon the state of two /// different animations. Color? _getBackgroundColor(ThemeData theme, ChipThemeData chipTheme, ChipThemeData chipDefaults) { if (theme.useMaterial3) { + final Color? disabledColor = resolveColor( + color: widget.color ?? chipTheme.color, + disabledColor: widget.disabledColor ?? chipTheme.disabledColor, + defaultColor: chipDefaults.color, + ); + final Color? backgroundColor = resolveColor( + color: widget.color ?? chipTheme.color, + backgroundColor: widget.backgroundColor ?? chipTheme.backgroundColor, + defaultColor: chipDefaults.color, + ); + final Color? selectedColor = resolveColor( + color: widget.color ?? chipTheme.color, + selectedColor: widget.selectedColor ?? chipTheme.selectedColor, + defaultColor: chipDefaults.color, + ); final ColorTween backgroundTween = ColorTween( - begin: widget.disabledColor - ?? chipTheme.disabledColor - ?? chipDefaults.disabledColor, - end: widget.backgroundColor - ?? chipTheme.backgroundColor - ?? chipDefaults.backgroundColor, + begin: disabledColor, + end: backgroundColor, ); final ColorTween selectTween = ColorTween( begin: backgroundTween.evaluate(enableController), - end: widget.selectedColor - ?? chipTheme.selectedColor - ?? chipDefaults.selectedColor, + end: selectedColor, ); return selectTween.evaluate(selectionFade); } else { @@ -1295,6 +1333,37 @@ class _RawChipState extends State with MaterialStateMixin, TickerProvid } } +class _IndividualOverrides extends MaterialStateProperty { + _IndividualOverrides({ + this.color, + this.backgroundColor, + this.selectedColor, + this.disabledColor, + }); + + final MaterialStateProperty? color; + final Color? backgroundColor; + final Color? selectedColor; + final Color? disabledColor; + + @override + Color? resolve(Set states) { + if (color != null) { + return color!.resolve(states); + } + if (states.contains(MaterialState.selected) && states.contains(MaterialState.disabled)) { + return selectedColor; + } + if (states.contains(MaterialState.disabled)) { + return disabledColor; + } + if (states.contains(MaterialState.selected)) { + return selectedColor; + } + return backgroundColor; + } +} + /// Redirects the [buttonRect.dy] passed to [RenderBox.hitTest] to the vertical /// center of the widget. /// @@ -2176,7 +2245,7 @@ class _ChipDefaultsM3 extends ChipThemeData { TextStyle? get labelStyle => _textTheme.labelLarge; @override - Color? get backgroundColor => null; + MaterialStateProperty? get color => null; // Subclasses override this getter @override Color? get shadowColor => Colors.transparent; @@ -2184,15 +2253,9 @@ class _ChipDefaultsM3 extends ChipThemeData { @override Color? get surfaceTintColor => _colors.surfaceTint; - @override - Color? get selectedColor => null; - @override Color? get checkmarkColor => null; - @override - Color? get disabledColor => null; - @override Color? get deleteIconColor => null; diff --git a/packages/flutter/lib/src/material/chip_theme.dart b/packages/flutter/lib/src/material/chip_theme.dart index 32213d5908..4c9a052d9e 100644 --- a/packages/flutter/lib/src/material/chip_theme.dart +++ b/packages/flutter/lib/src/material/chip_theme.dart @@ -9,6 +9,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'colors.dart'; +import 'material_state.dart'; import 'theme.dart'; /// Applies a chip theme to descendant [RawChip]-based widgets, like [Chip], @@ -178,6 +179,7 @@ class ChipThemeData with Diagnosticable { /// This will rarely be used directly. It is used by [lerp] to /// create intermediate themes based on two themes. const ChipThemeData({ + this.color, this.backgroundColor, this.deleteIconColor, this.disabledColor, @@ -268,6 +270,12 @@ class ChipThemeData with Diagnosticable { ); } + /// Overrides the default for [ChipAttributes.color]. + /// + /// This property applies to [ActionChip], [Chip], [ChoiceChip], + /// [FilterChip], [InputChip], [RawChip]. + final MaterialStateProperty? color; + /// Overrides the default for [ChipAttributes.backgroundColor] /// which is used for unselected, enabled chip backgrounds. /// @@ -433,6 +441,7 @@ class ChipThemeData with Diagnosticable { /// Creates a copy of this object but with the given fields replaced with the /// new values. ChipThemeData copyWith({ + MaterialStateProperty? color, Color? backgroundColor, Color? deleteIconColor, Color? disabledColor, @@ -455,6 +464,7 @@ class ChipThemeData with Diagnosticable { IconThemeData? iconTheme, }) { return ChipThemeData( + color: color ?? this.color, backgroundColor: backgroundColor ?? this.backgroundColor, deleteIconColor: deleteIconColor ?? this.deleteIconColor, disabledColor: disabledColor ?? this.disabledColor, @@ -488,6 +498,7 @@ class ChipThemeData with Diagnosticable { return a; } return ChipThemeData( + color: MaterialStateProperty.lerp(a?.color, b?.color, t, Color.lerp), backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t), deleteIconColor: Color.lerp(a?.deleteIconColor, b?.deleteIconColor, t), disabledColor: Color.lerp(a?.disabledColor, b?.disabledColor, t), @@ -537,6 +548,7 @@ class ChipThemeData with Diagnosticable { @override int get hashCode => Object.hashAll([ + color, backgroundColor, deleteIconColor, disabledColor, @@ -568,6 +580,7 @@ class ChipThemeData with Diagnosticable { return false; } return other is ChipThemeData + && other.color == color && other.backgroundColor == backgroundColor && other.deleteIconColor == deleteIconColor && other.disabledColor == disabledColor @@ -593,6 +606,7 @@ class ChipThemeData with Diagnosticable { @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); + properties.add(DiagnosticsProperty>('color', color, defaultValue: null)); properties.add(ColorProperty('backgroundColor', backgroundColor, defaultValue: null)); properties.add(ColorProperty('deleteIconColor', deleteIconColor, defaultValue: null)); properties.add(ColorProperty('disabledColor', disabledColor, defaultValue: null)); diff --git a/packages/flutter/lib/src/material/choice_chip.dart b/packages/flutter/lib/src/material/choice_chip.dart index 50f4357bcb..7b05db3622 100644 --- a/packages/flutter/lib/src/material/choice_chip.dart +++ b/packages/flutter/lib/src/material/choice_chip.dart @@ -10,6 +10,7 @@ import 'chip_theme.dart'; import 'color_scheme.dart'; import 'colors.dart'; import 'debug.dart'; +import 'material_state.dart'; import 'text_theme.dart'; import 'theme.dart'; import 'theme_data.dart'; @@ -78,6 +79,7 @@ class ChoiceChip extends StatelessWidget this.clipBehavior = Clip.none, this.focusNode, this.autofocus = false, + this.color, this.backgroundColor, this.padding, this.visualDensity, @@ -116,6 +118,7 @@ class ChoiceChip extends StatelessWidget this.clipBehavior = Clip.none, this.focusNode, this.autofocus = false, + this.color, this.backgroundColor, this.padding, this.visualDensity, @@ -163,6 +166,8 @@ class ChoiceChip extends StatelessWidget @override final bool autofocus; @override + final MaterialStateProperty? color; + @override final Color? backgroundColor; @override final EdgeInsetsGeometry? padding; @@ -218,6 +223,7 @@ class ChoiceChip extends StatelessWidget autofocus: autofocus, disabledColor: disabledColor, selectedColor: selectedColor ?? chipTheme.secondarySelectedColor, + color: color, backgroundColor: backgroundColor, padding: padding, visualDensity: visualDensity, @@ -270,7 +276,27 @@ class _ChoiceChipDefaultsM3 extends ChipThemeData { TextStyle? get labelStyle => _textTheme.labelLarge; @override - Color? get backgroundColor => null; + MaterialStateProperty? get color => + MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.selected) && states.contains(MaterialState.disabled)) { + return _chipVariant == _ChipVariant.flat + ? _colors.onSurface.withOpacity(0.12) + : _colors.onSurface.withOpacity(0.12); + } + if (states.contains(MaterialState.disabled)) { + return _chipVariant == _ChipVariant.flat + ? null + : _colors.onSurface.withOpacity(0.12); + } + if (states.contains(MaterialState.selected)) { + return _chipVariant == _ChipVariant.flat + ? _colors.secondaryContainer + : _colors.secondaryContainer; + } + return _chipVariant == _ChipVariant.flat + ? null + : null; + }); @override Color? get shadowColor => _chipVariant == _ChipVariant.flat @@ -280,25 +306,9 @@ class _ChoiceChipDefaultsM3 extends ChipThemeData { @override Color? get surfaceTintColor => _colors.surfaceTint; - @override - Color? get selectedColor => _chipVariant == _ChipVariant.flat - ? isEnabled - ? _colors.secondaryContainer - : _colors.onSurface.withOpacity(0.12) - : isEnabled - ? _colors.secondaryContainer - : _colors.onSurface.withOpacity(0.12); - @override Color? get checkmarkColor => _colors.onSecondaryContainer; - @override - Color? get disabledColor => _chipVariant == _ChipVariant.flat - ? isSelected - ? _colors.onSurface.withOpacity(0.12) - : null - : _colors.onSurface.withOpacity(0.12); - @override Color? get deleteIconColor => _colors.onSecondaryContainer; diff --git a/packages/flutter/lib/src/material/filter_chip.dart b/packages/flutter/lib/src/material/filter_chip.dart index 6753847ba2..887bb05c87 100644 --- a/packages/flutter/lib/src/material/filter_chip.dart +++ b/packages/flutter/lib/src/material/filter_chip.dart @@ -10,6 +10,7 @@ import 'chip_theme.dart'; import 'color_scheme.dart'; import 'colors.dart'; import 'debug.dart'; +import 'material_state.dart'; import 'text_theme.dart'; import 'theme.dart'; import 'theme_data.dart'; @@ -80,6 +81,7 @@ class FilterChip extends StatelessWidget this.clipBehavior = Clip.none, this.focusNode, this.autofocus = false, + this.color, this.backgroundColor, this.padding, this.visualDensity, @@ -118,6 +120,7 @@ class FilterChip extends StatelessWidget this.clipBehavior = Clip.none, this.focusNode, this.autofocus = false, + this.color, this.backgroundColor, this.padding, this.visualDensity, @@ -165,6 +168,8 @@ class FilterChip extends StatelessWidget @override final bool autofocus; @override + final MaterialStateProperty? color; + @override final Color? backgroundColor; @override final EdgeInsetsGeometry? padding; @@ -215,6 +220,7 @@ class FilterChip extends StatelessWidget clipBehavior: clipBehavior, focusNode: focusNode, autofocus: autofocus, + color: color, backgroundColor: backgroundColor, disabledColor: disabledColor, selectedColor: selectedColor, @@ -270,7 +276,27 @@ class _FilterChipDefaultsM3 extends ChipThemeData { TextStyle? get labelStyle => _textTheme.labelLarge; @override - Color? get backgroundColor => null; + MaterialStateProperty? get color => + MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.selected) && states.contains(MaterialState.disabled)) { + return _chipVariant == _ChipVariant.flat + ? _colors.onSurface.withOpacity(0.12) + : _colors.onSurface.withOpacity(0.12); + } + if (states.contains(MaterialState.disabled)) { + return _chipVariant == _ChipVariant.flat + ? null + : _colors.onSurface.withOpacity(0.12); + } + if (states.contains(MaterialState.selected)) { + return _chipVariant == _ChipVariant.flat + ? _colors.secondaryContainer + : _colors.secondaryContainer; + } + return _chipVariant == _ChipVariant.flat + ? null + : null; + }); @override Color? get shadowColor => _chipVariant == _ChipVariant.flat @@ -280,25 +306,9 @@ class _FilterChipDefaultsM3 extends ChipThemeData { @override Color? get surfaceTintColor => _colors.surfaceTint; - @override - Color? get selectedColor => _chipVariant == _ChipVariant.flat - ? isEnabled - ? _colors.secondaryContainer - : _colors.onSurface.withOpacity(0.12) - : isEnabled - ? _colors.secondaryContainer - : _colors.onSurface.withOpacity(0.12); - @override Color? get checkmarkColor => _colors.onSecondaryContainer; - @override - Color? get disabledColor => _chipVariant == _ChipVariant.flat - ? isSelected - ? _colors.onSurface.withOpacity(0.12) - : null - : _colors.onSurface.withOpacity(0.12); - @override Color? get deleteIconColor => _colors.onSecondaryContainer; diff --git a/packages/flutter/lib/src/material/input_chip.dart b/packages/flutter/lib/src/material/input_chip.dart index 53974e4bfe..756d557ee1 100644 --- a/packages/flutter/lib/src/material/input_chip.dart +++ b/packages/flutter/lib/src/material/input_chip.dart @@ -11,6 +11,7 @@ import 'color_scheme.dart'; import 'colors.dart'; import 'debug.dart'; import 'icons.dart'; +import 'material_state.dart'; import 'text_theme.dart'; import 'theme.dart'; import 'theme_data.dart'; @@ -99,6 +100,7 @@ class InputChip extends StatelessWidget this.clipBehavior = Clip.none, this.focusNode, this.autofocus = false, + this.color, this.backgroundColor, this.padding, this.visualDensity, @@ -162,6 +164,8 @@ class InputChip extends StatelessWidget @override final bool autofocus; @override + final MaterialStateProperty? color; + @override final Color? backgroundColor; @override final EdgeInsetsGeometry? padding; @@ -223,6 +227,7 @@ class InputChip extends StatelessWidget clipBehavior: clipBehavior, focusNode: focusNode, autofocus: autofocus, + color: color, backgroundColor: backgroundColor, padding: padding, visualDensity: visualDensity, @@ -264,7 +269,19 @@ class _InputChipDefaultsM3 extends ChipThemeData { TextStyle? get labelStyle => _textTheme.labelLarge; @override - Color? get backgroundColor => null; + MaterialStateProperty? get color => + MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.selected) && states.contains(MaterialState.disabled)) { + return _colors.onSurface.withOpacity(0.12); + } + if (states.contains(MaterialState.disabled)) { + return null; + } + if (states.contains(MaterialState.selected)) { + return _colors.secondaryContainer; + } + return null; + }); @override Color? get shadowColor => Colors.transparent; @@ -272,17 +289,9 @@ class _InputChipDefaultsM3 extends ChipThemeData { @override Color? get surfaceTintColor => Colors.transparent; - @override - Color? get selectedColor => isEnabled - ? _colors.secondaryContainer - : _colors.onSurface.withOpacity(0.12); - @override Color? get checkmarkColor => null; - @override - Color? get disabledColor => null; - @override Color? get deleteIconColor => _colors.onSecondaryContainer; diff --git a/packages/flutter/test/material/action_chip_test.dart b/packages/flutter/test/material/action_chip_test.dart index 92e0c1a842..f3f4d67fc1 100644 --- a/packages/flutter/test/material/action_chip_test.dart +++ b/packages/flutter/test/material/action_chip_test.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import '../foundation/leak_tracking.dart'; +import '../rendering/mock_canvas.dart'; /// Adds the basic requirements for a Chip. Widget wrapForChip({ @@ -13,9 +14,10 @@ Widget wrapForChip({ TextDirection textDirection = TextDirection.ltr, double textScaleFactor = 1.0, Brightness brightness = Brightness.light, + bool? useMaterial3, }) { return MaterialApp( - theme: ThemeData(brightness: brightness), + theme: ThemeData(brightness: brightness, useMaterial3: useMaterial3), home: Directionality( textDirection: textDirection, child: MediaQuery( @@ -26,6 +28,15 @@ Widget wrapForChip({ ); } +RenderBox getMaterialBox(WidgetTester tester, Finder type) { + return tester.firstRenderObject( + find.descendant( + of: type, + matching: find.byType(CustomPaint), + ), + ); +} + Material getMaterial(WidgetTester tester) { return tester.widget( find.descendant( @@ -196,6 +207,120 @@ void main() { expect(decoration.color, theme.colorScheme.onSurface.withOpacity(0.12)); }); + testWidgets('ActionChip.color resolves material states', (WidgetTester tester) async { + const Color disabledColor = Color(0xff00ff00); + const Color backgroundColor = Color(0xff0000ff); + final MaterialStateProperty color = MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return disabledColor; + } + return backgroundColor; + }); + Widget buildApp({ required bool enabled, required bool selected }) { + return wrapForChip( + useMaterial3: true, + child: Column( + children: [ + ActionChip( + onPressed: enabled ? () { } : null, + color: color, + label: const Text('ActionChip'), + ), + ActionChip.elevated( + onPressed: enabled ? () { } : null, + color: color, + label: const Text('ActionChip.elevated'), + ), + ], + ), + ); + } + + // Test enabled state. + await tester.pumpWidget(buildApp(enabled: true, selected: false)); + + // Enabled ActionChip should have the provided backgroundColor. + expect( + getMaterialBox(tester, find.byType(RawChip).first), + paints..rrect(color: backgroundColor), + ); + // Enabled elevated ActionChip should have the provided backgroundColor. + expect( + getMaterialBox(tester, find.byType(RawChip).last), + paints..rrect(color: backgroundColor), + ); + + // Test disabled state. + await tester.pumpWidget(buildApp(enabled: false, selected: false)); + await tester.pumpAndSettle(); + + // Disabled ActionChip should have the provided disabledColor. + expect( + getMaterialBox(tester, find.byType(RawChip).first), + paints..rrect(color: disabledColor), + ); + // Disabled elevated ActionChip should have the provided disabledColor. + expect( + getMaterialBox(tester, find.byType(RawChip).last), + paints..rrect(color: disabledColor), + ); + }); + + testWidgets('ActionChip uses provided state color properties', (WidgetTester tester) async { + const Color disabledColor = Color(0xff00ff00); + const Color backgroundColor = Color(0xff0000ff); + Widget buildApp({ required bool enabled, required bool selected }) { + return wrapForChip( + useMaterial3: true, + child: Column( + children: [ + ActionChip( + onPressed: enabled ? () { } : null, + disabledColor: disabledColor, + backgroundColor: backgroundColor, + label: const Text('ActionChip'), + ), + ActionChip.elevated( + onPressed: enabled ? () { } : null, + disabledColor: disabledColor, + backgroundColor: backgroundColor, + label: const Text('ActionChip.elevated'), + ), + ], + ), + ); + } + + // Test enabled state. + await tester.pumpWidget(buildApp(enabled: true, selected: false)); + + // Enabled ActionChip should have the provided backgroundColor. + expect( + getMaterialBox(tester, find.byType(RawChip).first), + paints..rrect(color: backgroundColor), + ); + // Enabled elevated ActionChip should have the provided backgroundColor. + expect( + getMaterialBox(tester, find.byType(RawChip).last), + paints..rrect(color: backgroundColor), + ); + + // Test disabled state. + await tester.pumpWidget(buildApp(enabled: false, selected: false)); + await tester.pumpAndSettle(); + + // Disabled ActionChip should have the provided disabledColor. + expect( + getMaterialBox(tester, find.byType(RawChip).first), + paints..rrect(color: disabledColor), + ); + // Disabled elevated ActionChip should have the provided disabledColor. + expect( + getMaterialBox(tester, find.byType(RawChip).last), + paints..rrect(color: disabledColor), + ); + }); + testWidgetsWithLeakTracking('ActionChip can be tapped', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( diff --git a/packages/flutter/test/material/chip_test.dart b/packages/flutter/test/material/chip_test.dart index 53b54d8f58..97051cb274 100644 --- a/packages/flutter/test/material/chip_test.dart +++ b/packages/flutter/test/material/chip_test.dart @@ -74,9 +74,10 @@ Widget wrapForChip({ TextDirection textDirection = TextDirection.ltr, double textScaleFactor = 1.0, Brightness brightness = Brightness.light, + bool? useMaterial3, }) { return MaterialApp( - theme: ThemeData(brightness: brightness, useMaterial3: false), + theme: ThemeData(brightness: brightness, useMaterial3: useMaterial3), home: Directionality( textDirection: textDirection, child: MediaQuery( @@ -3341,6 +3342,338 @@ void main() { ..rect(color: const Color(0x1f000000)), ); }); + + testWidgets('RawChip.color resolves material states', (WidgetTester tester) async { + const Color disabledSelectedColor = Color(0xffffff00); + const Color disabledColor = Color(0xff00ff00); + const Color backgroundColor = Color(0xff0000ff); + const Color selectedColor = Color(0xffff0000); + Widget buildApp({ required bool enabled, required bool selected }) { + return wrapForChip( + useMaterial3: true, + child: RawChip( + isEnabled: enabled, + selected: selected, + color: MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled) && states.contains(MaterialState.selected)) { + return disabledSelectedColor; + } + if (states.contains(MaterialState.disabled)) { + return disabledColor; + } + if (states.contains(MaterialState.selected)) { + return selectedColor; + } + return backgroundColor; + }), + label: const Text('RawChip'), + ), + ); + } + + // Test enabled chip. + await tester.pumpWidget(buildApp(enabled: true, selected: false)); + + // Enabled chip should have the provided backgroundColor. + expect(getMaterialBox(tester), paints..rrect(color: backgroundColor)); + + // Test disabled chip. + await tester.pumpWidget(buildApp(enabled: false, selected: false)); + await tester.pumpAndSettle(); + + // Disabled chip should have the provided disabledColor. + expect(getMaterialBox(tester), paints..rrect(color: disabledColor)); + + // Test enabled & selected chip. + await tester.pumpWidget(buildApp(enabled: true, selected: true)); + await tester.pumpAndSettle(); + + // Enabled & selected chip should have the provided selectedColor. + expect(getMaterialBox(tester), paints..rrect(color: selectedColor)); + + // Test disabled & selected chip. + await tester.pumpWidget(buildApp(enabled: false, selected: true)); + await tester.pumpAndSettle(); + + // Disabled & selected chip should have the provided disabledSelectedColor. + expect(getMaterialBox(tester), paints..rrect(color: disabledSelectedColor)); + }); + + testWidgets('RawChip uses provided state color properties', (WidgetTester tester) async { + const Color disabledColor = Color(0xff00ff00); + const Color backgroundColor = Color(0xff0000ff); + const Color selectedColor = Color(0xffff0000); + Widget buildApp({ required bool enabled, required bool selected }) { + return wrapForChip( + useMaterial3: true, + child: RawChip( + isEnabled: enabled, + selected: selected, + disabledColor: disabledColor, + backgroundColor: backgroundColor, + selectedColor: selectedColor, + label: const Text('RawChip'), + ), + ); + } + + // Test enabled chip. + await tester.pumpWidget(buildApp(enabled: true, selected: false)); + + // Enabled chip should have the provided backgroundColor. + expect(getMaterialBox(tester), paints..rrect(color: backgroundColor)); + + // Test disabled chip. + await tester.pumpWidget(buildApp(enabled: false, selected: false)); + await tester.pumpAndSettle(); + + // Disabled chip should have the provided disabledColor. + expect(getMaterialBox(tester), paints..rrect(color: disabledColor)); + + // Test enabled & selected chip. + await tester.pumpWidget(buildApp(enabled: true, selected: true)); + await tester.pumpAndSettle(); + + // Enabled & selected chip should have the provided selectedColor. + expect(getMaterialBox(tester), paints..rrect(color: selectedColor)); + }); + + group('Material 2', () { + // These tests are only relevant for Material 2. Once Material 2 + // support is deprecated and the APIs are removed, these tests + // can be deleted. + + testWidgets('M2 Chip defaults', (WidgetTester tester) async { + late TextTheme textTheme; + + Widget buildFrame(Brightness brightness) { + return MaterialApp( + theme: ThemeData(brightness: brightness, useMaterial3: false), + home: Scaffold( + body: Center( + child: Builder( + builder: (BuildContext context) { + textTheme = Theme.of(context).textTheme; + return Chip( + avatar: const CircleAvatar(child: Text('A')), + label: const Text('Chip A'), + onDeleted: () { }, + ); + }, + ), + ), + ), + ); + } + + await tester.pumpWidget(buildFrame(Brightness.light)); + expect(getMaterialBox(tester), paints..rrect()..circle(color: const Color(0xff1976d2))); + expect(tester.getSize(find.byType(Chip)), const Size(156.0, 48.0)); + expect(getMaterial(tester).color, null); + expect(getMaterial(tester).elevation, 0); + expect(getMaterial(tester).shape, const StadiumBorder()); + expect(getIconData(tester).color?.value, 0xffffffff); + expect(getIconData(tester).opacity, null); + expect(getIconData(tester).size, null); + + TextStyle labelStyle = getLabelStyle(tester, 'Chip A').style; + expect(labelStyle.color?.value, 0xde000000); + expect(labelStyle.fontFamily, textTheme.bodyLarge?.fontFamily); + expect(labelStyle.fontFamilyFallback, textTheme.bodyLarge?.fontFamilyFallback); + expect(labelStyle.fontFeatures, textTheme.bodyLarge?.fontFeatures); + expect(labelStyle.fontSize, textTheme.bodyLarge?.fontSize); + expect(labelStyle.fontStyle, textTheme.bodyLarge?.fontStyle); + expect(labelStyle.fontWeight, textTheme.bodyLarge?.fontWeight); + expect(labelStyle.height, textTheme.bodyLarge?.height); + expect(labelStyle.inherit, textTheme.bodyLarge?.inherit); + expect(labelStyle.leadingDistribution, textTheme.bodyLarge?.leadingDistribution); + expect(labelStyle.letterSpacing, textTheme.bodyLarge?.letterSpacing); + expect(labelStyle.overflow, textTheme.bodyLarge?.overflow); + expect(labelStyle.textBaseline, textTheme.bodyLarge?.textBaseline); + expect(labelStyle.wordSpacing, textTheme.bodyLarge?.wordSpacing); + + await tester.pumpWidget(buildFrame(Brightness.dark)); + await tester.pumpAndSettle(); // Theme transition animation + expect(getMaterialBox(tester), paints..rrect(color: const Color(0x1fffffff))); + expect(tester.getSize(find.byType(Chip)), const Size(156.0, 48.0)); + expect(getMaterial(tester).color, null); + expect(getMaterial(tester).elevation, 0); + expect(getMaterial(tester).shape, const StadiumBorder()); + expect(getIconData(tester).color?.value, 0xffffffff); + expect(getIconData(tester).opacity, null); + expect(getIconData(tester).size, null); + + labelStyle = getLabelStyle(tester, 'Chip A').style; + expect(labelStyle.color?.value, 0xdeffffff); + expect(labelStyle.fontFamily, textTheme.bodyLarge?.fontFamily); + expect(labelStyle.fontFamilyFallback, textTheme.bodyLarge?.fontFamilyFallback); + expect(labelStyle.fontFeatures, textTheme.bodyLarge?.fontFeatures); + expect(labelStyle.fontSize, textTheme.bodyLarge?.fontSize); + expect(labelStyle.fontStyle, textTheme.bodyLarge?.fontStyle); + expect(labelStyle.fontWeight, textTheme.bodyLarge?.fontWeight); + expect(labelStyle.height, textTheme.bodyLarge?.height); + expect(labelStyle.inherit, textTheme.bodyLarge?.inherit); + expect(labelStyle.leadingDistribution, textTheme.bodyLarge?.leadingDistribution); + expect(labelStyle.letterSpacing, textTheme.bodyLarge?.letterSpacing); + expect(labelStyle.overflow, textTheme.bodyLarge?.overflow); + expect(labelStyle.textBaseline, textTheme.bodyLarge?.textBaseline); + expect(labelStyle.wordSpacing, textTheme.bodyLarge?.wordSpacing); + }); + + testWidgets('Chip uses the right theme colors for the right components', (WidgetTester tester) async { + final ThemeData themeData = ThemeData( + platform: TargetPlatform.android, + primarySwatch: Colors.blue, + useMaterial3: false, + ); + final ChipThemeData defaultChipTheme = ChipThemeData.fromDefaults( + brightness: themeData.brightness, + secondaryColor: Colors.blue, + labelStyle: themeData.textTheme.bodyLarge!, + ); + bool value = false; + Widget buildApp({ + ChipThemeData? chipTheme, + Widget? avatar, + Widget? deleteIcon, + bool isSelectable = true, + bool isPressable = false, + bool isDeletable = true, + bool showCheckmark = true, + }) { + chipTheme ??= defaultChipTheme; + return wrapForChip( + child: Theme( + data: themeData, + child: ChipTheme( + data: chipTheme, + child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) { + return RawChip( + showCheckmark: showCheckmark, + onDeleted: isDeletable ? () { } : null, + avatar: avatar, + deleteIcon: deleteIcon, + isEnabled: isSelectable || isPressable, + shape: chipTheme?.shape, + selected: isSelectable && value, + label: Text('$value'), + onSelected: isSelectable + ? (bool newValue) { + setState(() { + value = newValue; + }); + } + : null, + onPressed: isPressable + ? () { + setState(() { + value = true; + }); + } + : null, + ); + }), + ), + ), + ); + } + + await tester.pumpWidget(buildApp()); + + RenderBox materialBox = getMaterialBox(tester); + IconThemeData iconData = getIconData(tester); + DefaultTextStyle labelStyle = getLabelStyle(tester, 'false'); + + // Check default theme for enabled chip. + expect(materialBox, paints..rrect(color: defaultChipTheme.backgroundColor)); + expect(iconData.color, equals(const Color(0xde000000))); + expect(labelStyle.style.color, equals(Colors.black.withAlpha(0xde))); + + // Check default theme for disabled chip. + await tester.pumpWidget(buildApp(isSelectable: false)); + await tester.pumpAndSettle(); + materialBox = getMaterialBox(tester); + labelStyle = getLabelStyle(tester, 'false'); + expect(materialBox, paints..rrect(color: defaultChipTheme.disabledColor)); + expect(labelStyle.style.color, equals(Colors.black.withAlpha(0xde))); + + // Check default theme for enabled and selected chip. + await tester.pumpWidget(buildApp()); + await tester.pumpAndSettle(); + await tester.tap(find.byType(RawChip)); + await tester.pumpAndSettle(); + materialBox = getMaterialBox(tester); + expect(materialBox, paints..rrect(color: defaultChipTheme.selectedColor)); + + // Check default theme for disabled and selected chip. + await tester.pumpWidget(buildApp(isSelectable: false)); + await tester.pumpAndSettle(); + materialBox = getMaterialBox(tester); + labelStyle = getLabelStyle(tester, 'true'); + expect(materialBox, paints..rrect(color: defaultChipTheme.disabledColor)); + expect(labelStyle.style.color, equals(Colors.black.withAlpha(0xde))); + + // Enable the chip again. + await tester.pumpWidget(buildApp()); + await tester.pumpAndSettle(); + // Tap to unselect the chip. + await tester.tap(find.byType(RawChip)); + await tester.pumpAndSettle(); + + // Apply a custom theme. + const Color customColor1 = Color(0xcafefeed); + const Color customColor2 = Color(0xdeadbeef); + const Color customColor3 = Color(0xbeefcafe); + const Color customColor4 = Color(0xaddedabe); + final ChipThemeData customTheme = defaultChipTheme.copyWith( + brightness: Brightness.dark, + backgroundColor: customColor1, + disabledColor: customColor2, + selectedColor: customColor3, + deleteIconColor: customColor4, + ); + await tester.pumpWidget(buildApp(chipTheme: customTheme)); + await tester.pumpAndSettle(); + materialBox = getMaterialBox(tester); + iconData = getIconData(tester); + labelStyle = getLabelStyle(tester, 'false'); + + // Check custom theme for enabled chip. + expect(materialBox, paints..rrect(color: customTheme.backgroundColor)); + expect(iconData.color, equals(customTheme.deleteIconColor)); + expect(labelStyle.style.color, equals(Colors.black.withAlpha(0xde))); + + // Check custom theme with disabled widget. + await tester.pumpWidget(buildApp( + chipTheme: customTheme, + isSelectable: false, + )); + await tester.pumpAndSettle(); + materialBox = getMaterialBox(tester); + labelStyle = getLabelStyle(tester, 'false'); + expect(materialBox, paints..rrect(color: customTheme.disabledColor)); + expect(labelStyle.style.color, equals(Colors.black.withAlpha(0xde))); + + // Check custom theme for enabled and selected chip. + await tester.pumpWidget(buildApp(chipTheme: customTheme)); + await tester.pumpAndSettle(); + await tester.tap(find.byType(RawChip)); + await tester.pumpAndSettle(); + materialBox = getMaterialBox(tester); + expect(materialBox, paints..rrect(color: customTheme.selectedColor)); + + // Check custom theme for disabled and selected chip. + await tester.pumpWidget(buildApp( + chipTheme: customTheme, + isSelectable: false, + )); + await tester.pumpAndSettle(); + materialBox = getMaterialBox(tester); + labelStyle = getLabelStyle(tester, 'true'); + expect(materialBox, paints..rrect(color: customTheme.disabledColor)); + expect(labelStyle.style.color, equals(Colors.black.withAlpha(0xde))); + }); + }); } class _MaterialStateOutlinedBorder extends StadiumBorder implements MaterialStateOutlinedBorder { diff --git a/packages/flutter/test/material/chip_theme_test.dart b/packages/flutter/test/material/chip_theme_test.dart index ae3b0d93a6..85e901b2a1 100644 --- a/packages/flutter/test/material/chip_theme_test.dart +++ b/packages/flutter/test/material/chip_theme_test.dart @@ -50,6 +50,7 @@ void main() { test('ChipThemeData defaults', () { const ChipThemeData themeData = ChipThemeData(); + expect(themeData.color, null); expect(themeData.backgroundColor, null); expect(themeData.deleteIconColor, null); expect(themeData.disabledColor, null); @@ -86,16 +87,17 @@ void main() { testWidgets('ChipThemeData implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const ChipThemeData( - backgroundColor: Color(0xfffffff0), - deleteIconColor: Color(0xfffffff1), - disabledColor: Color(0xfffffff2), - selectedColor: Color(0xfffffff3), - secondarySelectedColor: Color(0xfffffff4), - shadowColor: Color(0xfffffff5), - surfaceTintColor: Color(0xfffffff8), - selectedShadowColor: Color(0xfffffff6), + color: MaterialStatePropertyAll(Color(0xfffffff0)), + backgroundColor: Color(0xfffffff1), + deleteIconColor: Color(0xfffffff2), + disabledColor: Color(0xfffffff3), + selectedColor: Color(0xfffffff4), + secondarySelectedColor: Color(0xfffffff5), + shadowColor: Color(0xfffffff6), + surfaceTintColor: Color(0xfffffff7), + selectedShadowColor: Color(0xfffffff8), showCheckmark: true, - checkmarkColor: Color(0xfffffff7), + checkmarkColor: Color(0xfffffff9), labelPadding: EdgeInsets.all(1), padding: EdgeInsets.all(2), side: BorderSide(width: 10), @@ -113,16 +115,17 @@ void main() { .toList(); expect(description, [ - 'backgroundColor: Color(0xfffffff0)', - 'deleteIconColor: Color(0xfffffff1)', - 'disabledColor: Color(0xfffffff2)', - 'selectedColor: Color(0xfffffff3)', - 'secondarySelectedColor: Color(0xfffffff4)', - 'shadowColor: Color(0xfffffff5)', - 'surfaceTintColor: Color(0xfffffff8)', - 'selectedShadowColor: Color(0xfffffff6)', + 'color: MaterialStatePropertyAll(Color(0xfffffff0))', + 'backgroundColor: Color(0xfffffff1)', + 'deleteIconColor: Color(0xfffffff2)', + 'disabledColor: Color(0xfffffff3)', + 'selectedColor: Color(0xfffffff4)', + 'secondarySelectedColor: Color(0xfffffff5)', + 'shadowColor: Color(0xfffffff6)', + 'surfaceTintColor: Color(0xfffffff7)', + 'selectedShadowColor: Color(0xfffffff8)', 'showCheckmark: true', - 'checkMarkColor: Color(0xfffffff7)', + 'checkMarkColor: Color(0xfffffff9)', 'labelPadding: EdgeInsets.all(1.0)', 'padding: EdgeInsets.all(2.0)', 'side: BorderSide(width: 10.0)', @@ -330,7 +333,6 @@ void main() { expect(chipTheme.pressElevation, 8.0); }); - testWidgets('ChipThemeData generates correct opacities for defaults', (WidgetTester tester) async { const Color customColor1 = Color(0xcafefeed); const Color customColor2 = Color(0xdeadbeef); @@ -760,6 +762,111 @@ void main() { await tester.pumpWidget(chipWidget(selected: true)); expect(getMaterial(tester).shape, isA()); }); + + testWidgets('RawChip uses material state color from ChipTheme', (WidgetTester tester) async { + const Color disabledSelectedColor = Color(0xffffff00); + const Color disabledColor = Color(0xff00ff00); + const Color backgroundColor = Color(0xff0000ff); + const Color selectedColor = Color(0xffff0000); + Widget buildApp({ required bool enabled, required bool selected }) { + return MaterialApp( + theme: ThemeData( + chipTheme: ChipThemeData( + color: MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled) + && states.contains(MaterialState.selected)) { + return disabledSelectedColor; + } + if (states.contains(MaterialState.disabled)) { + return disabledColor; + } + if (states.contains(MaterialState.selected)) { + return selectedColor; + } + return backgroundColor; + }), + ), + useMaterial3: true, + ), + home: Material( + child: RawChip( + isEnabled: enabled, + selected: selected, + label: const Text('RawChip'), + ), + ), + ); + } + + // Check theme color for enabled chip. + await tester.pumpWidget(buildApp(enabled: true, selected: false)); + await tester.pumpAndSettle(); + + // Enabled chip should have the provided backgroundColor. + expect(getMaterialBox(tester), paints..rrect(color: backgroundColor)); + + // Check theme color for disabled chip. + await tester.pumpWidget(buildApp(enabled: false, selected: false)); + await tester.pumpAndSettle(); + + // Disabled chip should have the provided disabledColor. + expect(getMaterialBox(tester),paints..rrect(color: disabledColor)); + + // Check theme color for enabled and selected chip. + await tester.pumpWidget(buildApp(enabled: true, selected: true)); + await tester.pumpAndSettle(); + + // Enabled & selected chip should have the provided selectedColor. + expect(getMaterialBox(tester), paints..rrect(color: selectedColor)); + + // Check theme color for disabled & selected chip. + await tester.pumpWidget(buildApp(enabled: false, selected: true)); + await tester.pumpAndSettle(); + + // Disabled & selected chip should have the provided disabledSelectedColor. + expect(getMaterialBox(tester), paints..rrect(color: disabledSelectedColor)); + }); + + testWidgets('RawChip uses state colors from ChipTheme', (WidgetTester tester) async { + const ChipThemeData chipTheme = ChipThemeData( + disabledColor: Color(0xadfefafe), + backgroundColor: Color(0xcafefeed), + selectedColor: Color(0xbeefcafe), + ); + Widget buildApp({ required bool enabled, required bool selected }) { + return MaterialApp( + theme: ThemeData(chipTheme: chipTheme, useMaterial3: true), + home: Material( + child: RawChip( + isEnabled: enabled, + selected: selected, + label: const Text('RawChip'), + ), + ), + ); + } + + // Check theme color for enabled chip. + await tester.pumpWidget(buildApp(enabled: true, selected: false)); + await tester.pumpAndSettle(); + + // Enabled chip should have the provided backgroundColor. + expect(getMaterialBox(tester), paints..rrect(color: chipTheme.backgroundColor)); + + // Check theme color for disabled chip. + await tester.pumpWidget(buildApp(enabled: false, selected: false)); + await tester.pumpAndSettle(); + + // Disabled chip should have the provided disabledColor. + expect(getMaterialBox(tester),paints..rrect(color: chipTheme.disabledColor)); + + // Check theme color for enabled and selected chip. + await tester.pumpWidget(buildApp(enabled: true, selected: true)); + await tester.pumpAndSettle(); + + // Enabled & selected chip should have the provided selectedColor. + expect(getMaterialBox(tester), paints..rrect(color: chipTheme.selectedColor)); + }); } class _MaterialStateOutlinedBorder extends StadiumBorder implements MaterialStateOutlinedBorder { diff --git a/packages/flutter/test/material/choice_chip_test.dart b/packages/flutter/test/material/choice_chip_test.dart index 981d2b2936..187ec8fd2c 100644 --- a/packages/flutter/test/material/choice_chip_test.dart +++ b/packages/flutter/test/material/choice_chip_test.dart @@ -7,10 +7,10 @@ import 'package:flutter_test/flutter_test.dart'; import '../rendering/mock_canvas.dart'; -RenderBox getMaterialBox(WidgetTester tester) { +RenderBox getMaterialBox(WidgetTester tester, Finder type) { return tester.firstRenderObject( find.descendant( - of: find.byType(RawChip), + of: type, matching: find.byType(CustomPaint), ), ); @@ -19,7 +19,7 @@ RenderBox getMaterialBox(WidgetTester tester) { Material getMaterial(WidgetTester tester) { return tester.widget( find.descendant( - of: find.byType(RawChip), + of: find.byType(ChoiceChip), matching: find.byType(Material), ), ); @@ -40,9 +40,10 @@ Widget wrapForChip({ TextDirection textDirection = TextDirection.ltr, double textScaleFactor = 1.0, Brightness brightness = Brightness.light, + bool? useMaterial3, }) { return MaterialApp( - theme: ThemeData(brightness: brightness), + theme: ThemeData(brightness: brightness, useMaterial3: useMaterial3), home: Directionality( textDirection: textDirection, child: MediaQuery( @@ -327,6 +328,180 @@ void main() { expect(decoration.color, theme.colorScheme.onSurface.withOpacity(0.12)); }); + testWidgets('ChoiceChip.color resolves material states', (WidgetTester tester) async { + const Color disabledSelectedColor = Color(0xffffff00); + const Color disabledColor = Color(0xff00ff00); + const Color backgroundColor = Color(0xff0000ff); + const Color selectedColor = Color(0xffff0000); + final MaterialStateProperty color = MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled) && states.contains(MaterialState.selected)) { + return disabledSelectedColor; + } + if (states.contains(MaterialState.disabled)) { + return disabledColor; + } + if (states.contains(MaterialState.selected)) { + return selectedColor; + } + return backgroundColor; + }); + Widget buildApp({ required bool enabled, required bool selected }) { + return wrapForChip( + useMaterial3: true, + child: Column( + children: [ + ChoiceChip( + onSelected: enabled ? (bool value) { } : null, + selected: selected, + color: color, + label: const Text('ChoiceChip'), + ), + ChoiceChip.elevated( + onSelected: enabled ? (bool value) { } : null, + selected: selected, + color: color, + label: const Text('ChoiceChip.elevated'), + ), + ], + ), + ); + } + + // Test enabled state. + await tester.pumpWidget(buildApp(enabled: true, selected: false)); + + // Enabled ChoiceChip should have the provided backgroundColor. + expect( + getMaterialBox(tester, find.byType(RawChip).first), + paints..rrect(color: backgroundColor), + ); + // Enabled elevated ChoiceChip should have the provided backgroundColor. + expect( + getMaterialBox(tester, find.byType(RawChip).last), + paints..rrect(color: backgroundColor), + ); + + // Test disabled state. + await tester.pumpWidget(buildApp(enabled: false, selected: false)); + await tester.pumpAndSettle(); + + // Disabled ChoiceChip should have the provided disabledColor. + expect( + getMaterialBox(tester, find.byType(RawChip).first), + paints..rrect(color: disabledColor), + ); + // Disabled elevated ChoiceChip should have the provided disabledColor. + expect( + getMaterialBox(tester, find.byType(RawChip).last), + paints..rrect(color: disabledColor), + ); + + // Test enabled & selected state. + await tester.pumpWidget(buildApp(enabled: true, selected: true)); + await tester.pumpAndSettle(); + + // Enabled & selected ChoiceChip should have the provided selectedColor. + expect( + getMaterialBox(tester, find.byType(RawChip).first), + paints..rrect(color: selectedColor), + ); + // Enabled & selected elevated ChoiceChip should have the provided selectedColor. + expect( + getMaterialBox(tester, find.byType(RawChip).last), + paints..rrect(color: selectedColor), + ); + + // Test disabled & selected state. + await tester.pumpWidget(buildApp(enabled: false, selected: true)); + await tester.pumpAndSettle(); + + // Disabled & selected ChoiceChip should have the provided disabledSelectedColor. + expect( + getMaterialBox(tester, find.byType(RawChip).first), + paints..rrect(color: disabledSelectedColor), + ); + // Disabled & selected elevated ChoiceChip should have the provided disabledSelectedColor. + expect( + getMaterialBox(tester, find.byType(RawChip).last), + paints..rrect(color: disabledSelectedColor), + ); + }); + + testWidgets('ChoiceChip uses provided state color properties', (WidgetTester tester) async { + const Color disabledColor = Color(0xff00ff00); + const Color backgroundColor = Color(0xff0000ff); + const Color selectedColor = Color(0xffff0000); + Widget buildApp({ required bool enabled, required bool selected }) { + return wrapForChip( + useMaterial3: true, + child: Column( + children: [ + ChoiceChip( + onSelected: enabled ? (bool value) { } : null, + selected: selected, + disabledColor: disabledColor, + backgroundColor: backgroundColor, + selectedColor: selectedColor, + label: const Text('ChoiceChip'), + ), + ChoiceChip.elevated( + onSelected: enabled ? (bool value) { } : null, + selected: selected, + disabledColor: disabledColor, + backgroundColor: backgroundColor, + selectedColor: selectedColor, + label: const Text('ChoiceChip.elevated'), + ), + ], + ), + ); + } + + // Test enabled chips. + await tester.pumpWidget(buildApp(enabled: true, selected: false)); + + // Enabled ChoiceChip should have the provided backgroundColor. + expect( + getMaterialBox(tester, find.byType(RawChip).first), + paints..rrect(color: backgroundColor), + ); + // Enabled elevated ChoiceChip should have the provided backgroundColor. + expect( + getMaterialBox(tester, find.byType(RawChip).last), + paints..rrect(color: backgroundColor), + ); + + // Test disabled chips. + await tester.pumpWidget(buildApp(enabled: false, selected: false)); + await tester.pumpAndSettle(); + + // Disabled ChoiceChip should have the provided disabledColor. + expect( + getMaterialBox(tester, find.byType(RawChip).first), + paints..rrect(color: disabledColor), + ); + // Disabled elevated ChoiceChip should have the provided disabledColor. + expect( + getMaterialBox(tester, find.byType(RawChip).last), + paints..rrect(color: disabledColor), + ); + + // Test enabled & selected chips. + await tester.pumpWidget(buildApp(enabled: true, selected: true)); + await tester.pumpAndSettle(); + + // Enabled & selected ChoiceChip should have the provided selectedColor. + expect( + getMaterialBox(tester, find.byType(RawChip).first), + paints..rrect(color: selectedColor), + ); + // Enabled & selected elevated ChoiceChip should have the provided selectedColor. + expect( + getMaterialBox(tester, find.byType(RawChip).last), + paints..rrect(color: selectedColor), + ); + }); + testWidgets('ChoiceChip can be tapped', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( @@ -416,7 +591,7 @@ void main() { } await tester.pumpWidget(buildFrame(Brightness.light)); - expect(getMaterialBox(tester), paints..rrect(color: const Color(0x3d000000))); + expect(getMaterialBox(tester, find.byType(RawChip)), paints..rrect(color: const Color(0x3d000000))); expect(tester.getSize(find.byType(ChoiceChip)), const Size(108.0, 48.0)); expect(getMaterial(tester).color, null); expect(getMaterial(tester).elevation, 0); @@ -425,7 +600,7 @@ void main() { await tester.pumpWidget(buildFrame(Brightness.dark)); await tester.pumpAndSettle(); // Theme transition animation - expect(getMaterialBox(tester), paints..rrect(color: const Color(0x3dffffff))); + expect(getMaterialBox(tester, find.byType(RawChip)), paints..rrect(color: const Color(0x3dffffff))); expect(tester.getSize(find.byType(ChoiceChip)), const Size(108.0, 48.0)); expect(getMaterial(tester).color, null); expect(getMaterial(tester).elevation, 0); diff --git a/packages/flutter/test/material/filter_chip_test.dart b/packages/flutter/test/material/filter_chip_test.dart index 94c19e1d52..c4aaa40a06 100644 --- a/packages/flutter/test/material/filter_chip_test.dart +++ b/packages/flutter/test/material/filter_chip_test.dart @@ -78,6 +78,15 @@ void expectCheckmarkColor(Finder finder, Color color) { ); } +RenderBox getMaterialBox(WidgetTester tester, Finder type) { + return tester.firstRenderObject( + find.descendant( + of: type, + matching: find.byType(CustomPaint), + ), + ); +} + void checkChipMaterialClipBehavior(WidgetTester tester, Clip clipBehavior) { final Iterable materials = tester.widgetList(find.byType(Material)); // There should be two Material widgets, first Material is from the "_wrapForChip" and @@ -370,6 +379,181 @@ void main() { expect(decoration.color, theme.colorScheme.onSurface.withOpacity(0.12)); }); + testWidgets('FilterChip.color resolves material states', (WidgetTester tester) async { + const Color disabledSelectedColor = Color(0xffffff00); + const Color disabledColor = Color(0xff00ff00); + const Color backgroundColor = Color(0xff0000ff); + const Color selectedColor = Color(0xffff0000); + final MaterialStateProperty color = MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled) && states.contains(MaterialState.selected)) { + return disabledSelectedColor; + } + if (states.contains(MaterialState.disabled)) { + return disabledColor; + } + if (states.contains(MaterialState.selected)) { + return selectedColor; + } + return backgroundColor; + }); + Widget buildApp({ required bool enabled, required bool selected }) { + return wrapForChip( + useMaterial3: true, + child: Column( + children: [ + FilterChip( + onSelected: enabled ? (bool value) { } : null, + selected: selected, + color: color, + label: const Text('FilterChip'), + ), + FilterChip.elevated( + onSelected: enabled ? (bool value) { } : null, + selected: selected, + color: color, + label: const Text('FilterChip.elevated'), + ), + ], + ), + ); + } + + // Test enabled state. + await tester.pumpWidget(buildApp(enabled: true, selected: false)); + + // Enabled FilterChip should have the provided backgroundColor. + expect( + getMaterialBox(tester, find.byType(RawChip).first), + paints..rrect(color: backgroundColor), + ); + // Enabled elevated FilterChip should have the provided backgroundColor. + expect( + getMaterialBox(tester, find.byType(RawChip).last), + paints..rrect(color: backgroundColor), + ); + + // Test disabled state. + await tester.pumpWidget(buildApp(enabled: false, selected: false)); + await tester.pumpAndSettle(); + + // Disabled FilterChip should have the provided disabledColor. + expect( + getMaterialBox(tester, find.byType(RawChip).first), + paints..rrect(color: disabledColor), + ); + // Disabled elevated FilterChip should have the provided disabledColor. + expect( + getMaterialBox(tester, find.byType(RawChip).last), + paints..rrect(color: disabledColor), + ); + + // Test enabled & selected state. + await tester.pumpWidget(buildApp(enabled: true, selected: true)); + await tester.pumpAndSettle(); + + // Enabled & selected FilterChip should have the provided selectedColor. + expect( + getMaterialBox(tester, find.byType(RawChip).first), + paints..rrect(color: selectedColor), + ); + // Enabled & selected elevated FilterChip should have the provided selectedColor. + expect( + getMaterialBox(tester, find.byType(RawChip).last), + paints..rrect(color: selectedColor), + ); + + // Test disabled & selected state. + await tester.pumpWidget(buildApp(enabled: false, selected: true)); + await tester.pumpAndSettle(); + + // Disabled & selected FilterChip should have the provided disabledSelectedColor. + expect( + getMaterialBox(tester, find.byType(RawChip).first), + paints..rrect(color: disabledSelectedColor), + ); + // Disabled & selected elevated FilterChip should have the + // provided disabledSelectedColor. + expect( + getMaterialBox(tester, find.byType(RawChip).last), + paints..rrect(color: disabledSelectedColor), + ); + }); + + testWidgets('FilterChip uses provided state color properties', (WidgetTester tester) async { + const Color disabledColor = Color(0xff00ff00); + const Color backgroundColor = Color(0xff0000ff); + const Color selectedColor = Color(0xffff0000); + Widget buildApp({ required bool enabled, required bool selected }) { + return wrapForChip( + useMaterial3: true, + child: Column( + children: [ + FilterChip( + onSelected: enabled ? (bool value) { } : null, + selected: selected, + disabledColor: disabledColor, + backgroundColor: backgroundColor, + selectedColor: selectedColor, + label: const Text('FilterChip'), + ), + FilterChip.elevated( + onSelected: enabled ? (bool value) { } : null, + selected: selected, + disabledColor: disabledColor, + backgroundColor: backgroundColor, + selectedColor: selectedColor, + label: const Text('FilterChip.elevated'), + ), + ], + ), + ); + } + + // Test enabled state. + await tester.pumpWidget(buildApp(enabled: true, selected: false)); + + // Enabled FilterChip should have the provided backgroundColor. + expect( + getMaterialBox(tester, find.byType(RawChip).first), + paints..rrect(color: backgroundColor), + ); + // Enabled elevated FilterChip should have the provided backgroundColor. + expect( + getMaterialBox(tester, find.byType(RawChip).last), + paints..rrect(color: backgroundColor), + ); + + // Test disabled state. + await tester.pumpWidget(buildApp(enabled: false, selected: false)); + await tester.pumpAndSettle(); + + // Disabled FilterChip should have the provided disabledColor. + expect( + getMaterialBox(tester, find.byType(RawChip).first), + paints..rrect(color: disabledColor), + ); + // Disabled elevated FilterChip should have the provided disabledColor. + expect( + getMaterialBox(tester, find.byType(RawChip).last), + paints..rrect(color: disabledColor), + ); + + // Test enabled & selected state. + await tester.pumpWidget(buildApp(enabled: true, selected: true)); + await tester.pumpAndSettle(); + + // Enabled & selected FilterChip should have the provided selectedColor. + expect( + getMaterialBox(tester, find.byType(RawChip).first), + paints..rrect(color: selectedColor), + ); + // Enabled & selected elevated FilterChip should have the provided selectedColor. + expect( + getMaterialBox(tester, find.byType(RawChip).last), + paints..rrect(color: selectedColor), + ); + }); + testWidgets('FilterChip can be tapped', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( diff --git a/packages/flutter/test/material/input_chip_test.dart b/packages/flutter/test/material/input_chip_test.dart index a42ae2f0b0..827ded8084 100644 --- a/packages/flutter/test/material/input_chip_test.dart +++ b/packages/flutter/test/material/input_chip_test.dart @@ -13,7 +13,7 @@ Widget wrapForChip({ TextDirection textDirection = TextDirection.ltr, double textScaleFactor = 1.0, Brightness brightness = Brightness.light, - bool useMaterial3 = false, + bool? useMaterial3, }) { return MaterialApp( theme: ThemeData(brightness: brightness, useMaterial3: useMaterial3), @@ -101,6 +101,101 @@ void checkChipMaterialClipBehavior(WidgetTester tester, Clip clipBehavior) { } void main() { + testWidgets('InputChip.color resolves material states', (WidgetTester tester) async { + const Color disabledSelectedColor = Color(0xffffff00); + const Color disabledColor = Color(0xff00ff00); + const Color backgroundColor = Color(0xff0000ff); + const Color selectedColor = Color(0xffff0000); + Widget buildApp({ required bool enabled, required bool selected }) { + return wrapForChip( + useMaterial3: true, + child: InputChip( + onSelected: enabled ? (bool value) { } : null, + selected: selected, + color: MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled) && states.contains(MaterialState.selected)) { + return disabledSelectedColor; + } + if (states.contains(MaterialState.disabled)) { + return disabledColor; + } + if (states.contains(MaterialState.selected)) { + return selectedColor; + } + return backgroundColor; + }), + label: const Text('InputChip'), + ), + ); + } + + // Test enabled chip. + await tester.pumpWidget(buildApp(enabled: true, selected: false)); + + // Enabled chip should have the provided backgroundColor. + expect(getMaterialBox(tester), paints..rrect(color: backgroundColor)); + + // Test disabled chip. + await tester.pumpWidget(buildApp(enabled: false, selected: false)); + await tester.pumpAndSettle(); + + // Disabled chip should have the provided disabledColor. + expect(getMaterialBox(tester), paints..rrect(color: disabledColor)); + + // Test enabled & selected chip. + await tester.pumpWidget(buildApp(enabled: true, selected: true)); + await tester.pumpAndSettle(); + + // Enabled & selected chip should have the provided selectedColor. + expect(getMaterialBox(tester), paints..rrect(color: selectedColor)); + + // Test disabled & selected chip. + await tester.pumpWidget(buildApp(enabled: false, selected: true)); + await tester.pumpAndSettle(); + + // Disabled & selected chip should have the provided disabledSelectedColor. + expect(getMaterialBox(tester), paints..rrect(color: disabledSelectedColor)); + }); + + testWidgets('InputChip uses provided state color properties', (WidgetTester tester) async { + const Color disabledColor = Color(0xff00ff00); + const Color backgroundColor = Color(0xff0000ff); + const Color selectedColor = Color(0xffff0000); + Widget buildApp({ required bool enabled, required bool selected }) { + return wrapForChip( + useMaterial3: true, + child: InputChip( + onSelected: enabled ? (bool value) { } : null, + selected: selected, + disabledColor: disabledColor, + backgroundColor: backgroundColor, + selectedColor: selectedColor, + label: const Text('InputChip'), + ), + ); + } + + // Test enabled chip. + await tester.pumpWidget(buildApp(enabled: true, selected: false)); + + // Enabled chip should have the provided backgroundColor. + expect(getMaterialBox(tester), paints..rrect(color: backgroundColor)); + + // Test disabled chip. + await tester.pumpWidget(buildApp(enabled: false, selected: false)); + await tester.pumpAndSettle(); + + // Disabled chip should have the provided disabledColor. + expect(getMaterialBox(tester), paints..rrect(color: disabledColor)); + + // Test enabled & selected chip. + await tester.pumpWidget(buildApp(enabled: true, selected: true)); + await tester.pumpAndSettle(); + + // Enabled & selected chip should have the provided selectedColor. + expect(getMaterialBox(tester), paints..rrect(color: selectedColor)); + }); + testWidgets('InputChip can be tapped', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp(