Unverified Commit 9bbe89d8 authored by Craig Labenz's avatar Craig Labenz Committed by GitHub

Add MaterialStateBorderSide.resolveWith (#78731)

* added MaterialStateBorderSide.resolveWith

(Partially) Resolves #68596

* responded to comment nits

* reversed changes to text_buttons

* added test confirming compatibility with chips

* added MaterialStateBorderSide test for chip

* added intended usage for MaterialStateXYZ classes

* another docstring update

* corrected error in use case

* made resolvewith samples closures

* refined materialstatecolor example in docstring

* changed nullability in docstring and added null test

* added missing type in test

* fixed another typo in docstrings
parent 7ff0d14d
...@@ -104,6 +104,11 @@ typedef MaterialPropertyResolver<T> = T Function(Set<MaterialState> states); ...@@ -104,6 +104,11 @@ typedef MaterialPropertyResolver<T> = T Function(Set<MaterialState> states);
/// to provide a `defaultValue` to the super constructor, so that we can know /// to provide a `defaultValue` to the super constructor, so that we can know
/// at compile-time what its default color is. /// 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<Color>`
/// property values. [MaterialStateColor] should only be used with widgets that document
/// their support, like [TimePickerThemeData.dayPeriodColor].
///
/// {@tool snippet} /// {@tool snippet}
/// ///
/// This example defines a `MaterialStateColor` with a const constructor. /// This example defines a `MaterialStateColor` with a const constructor.
...@@ -295,26 +300,16 @@ class _EnabledAndDisabledMouseCursor extends MaterialStateMouseCursor { ...@@ -295,26 +300,16 @@ class _EnabledAndDisabledMouseCursor extends MaterialStateMouseCursor {
/// To use a [MaterialStateBorderSide], you should create a subclass of a /// To use a [MaterialStateBorderSide], you should create a subclass of a
/// [MaterialStateBorderSide] and override the abstract `resolve` method. /// [MaterialStateBorderSide] and override the abstract `resolve` method.
/// ///
/// This class enables existing widget implementations with [BorderSide]
/// properties to be extended to also effectively support `MaterialStateProperty<BorderSide>`
/// property values. [MaterialStateBorderSide] should only be used with widgets that document
/// their support, like [ActionChip.side].
///
/// {@tool dartpad --template=stateful_widget_material} /// {@tool dartpad --template=stateful_widget_material}
/// ///
/// This example defines a subclass of [MaterialStateBorderSide], that resolves /// This example defines a subclass of [MaterialStateBorderSide], that resolves
/// to a red border side when its widget is selected. /// to a red border side when its widget is selected.
/// ///
/// ```dart preamble
/// class RedSelectedBorderSide extends MaterialStateBorderSide {
/// @override
/// BorderSide? resolve(Set<MaterialState> 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 /// ```dart
/// bool isSelected = true; /// bool isSelected = true;
/// ///
...@@ -328,7 +323,12 @@ class _EnabledAndDisabledMouseCursor extends MaterialStateMouseCursor { ...@@ -328,7 +323,12 @@ class _EnabledAndDisabledMouseCursor extends MaterialStateMouseCursor {
/// isSelected = value; /// isSelected = value;
/// }); /// });
/// }, /// },
/// side: RedSelectedBorderSide(), /// side: MaterialStateBorderSide.resolveWith((Set<MaterialState> 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 ...@@ -346,6 +346,59 @@ abstract class MaterialStateBorderSide extends BorderSide implements MaterialSta
/// widget or theme. /// widget or theme.
@override @override
BorderSide? resolve(Set<MaterialState> states); BorderSide? resolve(Set<MaterialState> states);
/// Creates a [MaterialStateBorderSide] from a
/// [MaterialPropertyResolver<BorderSide?>] 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<MaterialState> 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<MaterialState> 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<BorderSide?> callback) =>
_MaterialStateBorderSide(callback);
}
/// A [MaterialStateBorderSide] created from a
/// [MaterialPropertyResolver<BorderSide>] 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<BorderSide?> _resolve;
@override
BorderSide? resolve(Set<MaterialState> states) {
return _resolve(states);
}
} }
/// Defines an [OutlinedBorder] whose value depends on a set of [MaterialState]s /// Defines an [OutlinedBorder] whose value depends on a set of [MaterialState]s
......
...@@ -2514,6 +2514,186 @@ void main() { ...@@ -2514,6 +2514,186 @@ void main() {
await gesture.removePointer(); 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<MaterialState> 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<MaterialState> 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 { testWidgets('Chip uses stateful shape in different states', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
OutlinedBorder? getShape(Set<MaterialState> states) { OutlinedBorder? getShape(Set<MaterialState> states) {
......
...@@ -466,6 +466,45 @@ void main() { ...@@ -466,6 +466,45 @@ void main() {
await gesture.removePointer(); 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<MaterialState> 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 { testWidgets('Chip uses stateful border side from chip theme', (WidgetTester tester) async {
const Color selectedColor = Color(0x00000001); const Color selectedColor = Color(0x00000001);
const Color defaultColor = Color(0x00000002); const Color defaultColor = Color(0x00000002);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment