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
|
/// 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 {
|
|||||||
/// 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 {
|
|||||||
/// 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
|
|||||||
/// 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() {
|
|||||||
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() {
|
|||||||
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);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user