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
This commit is contained in:
parent
7ff0d14d48
commit
9bbe89d8e7
@ -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
|
||||
/// 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}
|
||||
///
|
||||
/// 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<BorderSide>`
|
||||
/// 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<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
|
||||
/// bool isSelected = true;
|
||||
///
|
||||
@ -328,7 +323,12 @@ class _EnabledAndDisabledMouseCursor extends MaterialStateMouseCursor {
|
||||
/// 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
|
||||
/// widget or theme.
|
||||
@override
|
||||
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
|
||||
|
@ -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<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 {
|
||||
final FocusNode focusNode = FocusNode();
|
||||
OutlinedBorder? getShape(Set<MaterialState> states) {
|
||||
|
@ -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<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 {
|
||||
const Color selectedColor = Color(0x00000001);
|
||||
const Color defaultColor = Color(0x00000002);
|
||||
|
Loading…
x
Reference in New Issue
Block a user