diff --git a/packages/flutter/lib/src/material/material_state.dart b/packages/flutter/lib/src/material/material_state.dart index 530f3483a5..73c661d193 100644 --- a/packages/flutter/lib/src/material/material_state.dart +++ b/packages/flutter/lib/src/material/material_state.dart @@ -104,6 +104,11 @@ typedef MaterialPropertyResolver = T Function(Set states); /// to provide a `defaultValue` to the super constructor, so that we can know /// at compile-time what its default color is. /// +/// This class enables existing widget implementations with [Color] +/// properties to be extended to also effectively support `MaterialStateProperty` +/// property values. [MaterialStateColor] should only be used with widgets that document +/// their support, like [TimePickerThemeData.dayPeriodColor]. +/// /// {@tool snippet} /// /// This example defines a `MaterialStateColor` with a const constructor. @@ -295,26 +300,16 @@ class _EnabledAndDisabledMouseCursor extends MaterialStateMouseCursor { /// To use a [MaterialStateBorderSide], you should create a subclass of a /// [MaterialStateBorderSide] and override the abstract `resolve` method. /// +/// This class enables existing widget implementations with [BorderSide] +/// properties to be extended to also effectively support `MaterialStateProperty` +/// property values. [MaterialStateBorderSide] should only be used with widgets that document +/// their support, like [ActionChip.side]. +/// /// {@tool dartpad --template=stateful_widget_material} /// /// This example defines a subclass of [MaterialStateBorderSide], that resolves /// to a red border side when its widget is selected. /// -/// ```dart preamble -/// class RedSelectedBorderSide extends MaterialStateBorderSide { -/// @override -/// BorderSide? resolve(Set states) { -/// if (states.contains(MaterialState.selected)) { -/// return const BorderSide( -/// width: 1, -/// color: Colors.red, -/// ); -/// } -/// return null; // Defer to default value on the theme or widget. -/// } -/// } -/// ``` -/// /// ```dart /// bool isSelected = true; /// @@ -328,7 +323,12 @@ class _EnabledAndDisabledMouseCursor extends MaterialStateMouseCursor { /// isSelected = value; /// }); /// }, -/// side: RedSelectedBorderSide(), +/// side: MaterialStateBorderSide.resolveWith((Set states) { +/// if (states.contains(MaterialState.selected)) { +/// return const BorderSide(width: 1, color: Colors.red); +/// } +/// return null; // Defer to default value on the theme or widget. +/// }), /// ); /// } /// ``` @@ -346,6 +346,59 @@ abstract class MaterialStateBorderSide extends BorderSide implements MaterialSta /// widget or theme. @override BorderSide? resolve(Set states); + + /// Creates a [MaterialStateBorderSide] from a + /// [MaterialPropertyResolver] callback function. + /// + /// If used as a regular [BorderSide], the border resolved in the default state + /// (the empty set of states) will be used. + /// + /// Usage: + /// ```dart + /// ChipTheme( + /// data: Theme.of(context).chipTheme.copyWith( + /// side: MaterialStateBorderSide.resolveWith((Set states) { + /// if (states.contains(MaterialState.selected)) { + /// return const BorderSide(width: 1, color: Colors.red); + /// } + /// return null; // Defer to default value on the theme or widget. + /// }), + /// ), + /// child: Chip(), + /// ) + /// + /// // OR + /// + /// Chip( + /// ... + /// side: MaterialStateBorderSide.resolveWith((Set states) { + /// if (states.contains(MaterialState.selected)) { + /// return const BorderSide(width: 1, color: Colors.red); + /// } + /// return null; // Defer to default value on the theme or widget. + /// }), + /// ) + /// ``` + static MaterialStateBorderSide resolveWith(MaterialPropertyResolver callback) => + _MaterialStateBorderSide(callback); +} + +/// A [MaterialStateBorderSide] created from a +/// [MaterialPropertyResolver] callback alone. +/// +/// If used as a regular side, the side resolved in the default state will +/// be used. +/// +/// Used by [MaterialStateBorderSide.resolveWith]. +class _MaterialStateBorderSide extends MaterialStateBorderSide { + const _MaterialStateBorderSide(this._resolve); + + final MaterialPropertyResolver _resolve; + + @override + BorderSide? resolve(Set states) { + return _resolve(states); + } } /// Defines an [OutlinedBorder] whose value depends on a set of [MaterialState]s diff --git a/packages/flutter/test/material/chip_test.dart b/packages/flutter/test/material/chip_test.dart index 24402b5dde..29d96f2f71 100644 --- a/packages/flutter/test/material/chip_test.dart +++ b/packages/flutter/test/material/chip_test.dart @@ -2514,6 +2514,186 @@ void main() { await gesture.removePointer(); }); + testWidgets('Chip uses stateful border side color from resolveWith', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + + const Color pressedColor = Color(0x00000001); + const Color hoverColor = Color(0x00000002); + const Color focusedColor = Color(0x00000003); + const Color defaultColor = Color(0x00000004); + const Color selectedColor = Color(0x00000005); + const Color disabledColor = Color(0x00000006); + + BorderSide getBorderSide(Set states) { + Color sideColor = defaultColor; + + if (states.contains(MaterialState.disabled)) + sideColor = disabledColor; + + else if (states.contains(MaterialState.pressed)) + sideColor = pressedColor; + + else if (states.contains(MaterialState.hovered)) + sideColor = hoverColor; + + else if (states.contains(MaterialState.focused)) + sideColor = focusedColor; + + else if (states.contains(MaterialState.selected)) + sideColor = selectedColor; + + return BorderSide(color: sideColor, width: 1); + } + + Widget chipWidget({ bool enabled = true, bool selected = false }) { + return MaterialApp( + home: Scaffold( + body: Focus( + focusNode: focusNode, + child: ChoiceChip( + label: const Text('Chip'), + selected: selected, + onSelected: enabled ? (_) {} : null, + side: MaterialStateBorderSide.resolveWith(getBorderSide), + ), + ), + ), + ); + } + + // Default, not disabled. + await tester.pumpWidget(chipWidget()); + expect(find.byType(RawChip), paints..rrect(color: defaultColor)); + + // Selected. + await tester.pumpWidget(chipWidget(selected: true)); + expect(find.byType(RawChip), paints..rrect(color: selectedColor)); + + // Focused. + final FocusNode chipFocusNode = focusNode.children.first; + chipFocusNode.requestFocus(); + await tester.pumpAndSettle(); + expect(find.byType(RawChip), paints..rrect(color: focusedColor)); + + // Hovered. + final Offset center = tester.getCenter(find.byType(ChoiceChip)); + final TestGesture gesture = await tester.createGesture( + kind: PointerDeviceKind.mouse, + ); + await gesture.addPointer(); + await gesture.moveTo(center); + await tester.pumpAndSettle(); + expect(find.byType(RawChip), paints..rrect(color: hoverColor)); + + // Pressed. + await gesture.down(center); + await tester.pumpAndSettle(); + expect(find.byType(RawChip), paints..rrect(color: pressedColor)); + + // Disabled. + await tester.pumpWidget(chipWidget(enabled: false)); + await tester.pumpAndSettle(); + expect(find.byType(RawChip), paints..rrect(color: disabledColor)); + + // Teardown. + await gesture.removePointer(); + }); + + testWidgets('Chip uses stateful nullable border side color from resolveWith', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(); + + + const Color pressedColor = Color(0x00000001); + const Color hoverColor = Color(0x00000002); + const Color focusedColor = Color(0x00000003); + const Color defaultColor = Color(0x00000004); + const Color disabledColor = Color(0x00000006); + + const Color fallbackThemeColor = Color(0x00000007); + const BorderSide defaultBorderSide = BorderSide(color: fallbackThemeColor, width: 10.0); + + BorderSide? getBorderSide(Set states) { + Color sideColor = defaultColor; + + if (states.contains(MaterialState.disabled)) + sideColor = disabledColor; + + else if (states.contains(MaterialState.pressed)) + sideColor = pressedColor; + + else if (states.contains(MaterialState.hovered)) + sideColor = hoverColor; + + else if (states.contains(MaterialState.focused)) + sideColor = focusedColor; + + else if (states.contains(MaterialState.selected)) + return null; + + return BorderSide(color: sideColor, width: 1); + } + + Widget chipWidget({ bool enabled = true, bool selected = false }) { + return MaterialApp( + home: Scaffold( + body: Focus( + focusNode: focusNode, + child: ChipTheme( + data: ThemeData.light().chipTheme.copyWith( + side: defaultBorderSide, + ), + child: ChoiceChip( + label: const Text('Chip'), + selected: selected, + onSelected: enabled ? (_) {} : null, + side: MaterialStateBorderSide.resolveWith(getBorderSide), + ), + ), + ), + ), + ); + } + + // Default, not disabled. + await tester.pumpWidget(chipWidget()); + expect(find.byType(RawChip), paints..rrect(color: defaultColor)); + + // Selected. + await tester.pumpWidget(chipWidget(selected: true)); + // Because the resolver returns `null` for this value, we should fall back + // to the theme + expect(find.byType(RawChip), paints..rrect(color: fallbackThemeColor)); + + // Focused. + final FocusNode chipFocusNode = focusNode.children.first; + chipFocusNode.requestFocus(); + await tester.pumpAndSettle(); + expect(find.byType(RawChip), paints..rrect(color: focusedColor)); + + // Hovered. + final Offset center = tester.getCenter(find.byType(ChoiceChip)); + final TestGesture gesture = await tester.createGesture( + kind: PointerDeviceKind.mouse, + ); + await gesture.addPointer(); + await gesture.moveTo(center); + await tester.pumpAndSettle(); + expect(find.byType(RawChip), paints..rrect(color: hoverColor)); + + // Pressed. + await gesture.down(center); + await tester.pumpAndSettle(); + expect(find.byType(RawChip), paints..rrect(color: pressedColor)); + + // Disabled. + await tester.pumpWidget(chipWidget(enabled: false)); + await tester.pumpAndSettle(); + expect(find.byType(RawChip), paints..rrect(color: disabledColor)); + + // Teardown. + await gesture.removePointer(); + }); + testWidgets('Chip uses stateful shape in different states', (WidgetTester tester) async { final FocusNode focusNode = FocusNode(); OutlinedBorder? getShape(Set states) { diff --git a/packages/flutter/test/material/chip_theme_test.dart b/packages/flutter/test/material/chip_theme_test.dart index 4060210080..2de5b05c1f 100644 --- a/packages/flutter/test/material/chip_theme_test.dart +++ b/packages/flutter/test/material/chip_theme_test.dart @@ -466,6 +466,45 @@ void main() { await gesture.removePointer(); }); + testWidgets('Chip uses stateful border side from resolveWith pattern', (WidgetTester tester) async { + const Color selectedColor = Color(0x00000001); + const Color defaultColor = Color(0x00000002); + + BorderSide getBorderSide(Set states) { + Color color = defaultColor; + + if (states.contains(MaterialState.selected)) + color = selectedColor; + + return BorderSide(color: color, width: 1); + } + + Widget chipWidget({ bool selected = false }) { + return MaterialApp( + theme: ThemeData( + chipTheme: ThemeData.light().chipTheme.copyWith( + side: MaterialStateBorderSide.resolveWith(getBorderSide), + ), + ), + home: Scaffold( + body: ChoiceChip( + label: const Text('Chip'), + selected: selected, + onSelected: (_) {}, + ), + ), + ); + } + + // Default. + await tester.pumpWidget(chipWidget()); + expect(find.byType(RawChip), paints..rrect(color: defaultColor)); + + // Selected. + await tester.pumpWidget(chipWidget(selected: true)); + expect(find.byType(RawChip), paints..rrect(color: selectedColor)); + }); + testWidgets('Chip uses stateful border side from chip theme', (WidgetTester tester) async { const Color selectedColor = Color(0x00000001); const Color defaultColor = Color(0x00000002);