Unverified Commit 92d9630e authored by Per Classon's avatar Per Classon Committed by GitHub

Add side property to Chips, and resolve it and the state of Chips to be...

Add side property to Chips, and resolve it and the state of Chips to be MaterialState aware (#68596)
parent f03caeaf
......@@ -94,10 +94,39 @@ abstract class ChipAttributes {
/// * [MaterialState.pressed].
TextStyle? get labelStyle;
/// The [ShapeBorder] to draw around the chip.
/// The color and weight of the chip's outline.
///
/// Defaults to the shape in the ambient [ChipThemeData].
ShapeBorder? get shape;
/// Defaults to the border side in the ambient [ChipThemeData]. If the theme
/// border side resolves to null, the default is the border side of [shape].
///
/// This value is combined with [shape] to create a shape decorated with an
/// outline. If it is a [MaterialStateBorderSide],
/// [MaterialStateProperty.resolve] is used for the following
/// [MaterialState]s:
///
/// * [MaterialState.disabled].
/// * [MaterialState.selected].
/// * [MaterialState.hovered].
/// * [MaterialState.focused].
/// * [MaterialState.pressed].
BorderSide? get side;
/// The [OutlinedBorder] to draw around the chip.
///
/// Defaults to the shape in the ambient [ChipThemeData]. If the theme
/// shape resolves to null, the default is [StadiumBorder].
///
/// This shape is combined with [side] to create a shape decorated with an
/// outline. If it is a [MaterialStateOutlinedBorder],
/// [MaterialStateProperty.resolve] is used for the following
/// [MaterialState]s:
///
/// * [MaterialState.disabled].
/// * [MaterialState.selected].
/// * [MaterialState.hovered].
/// * [MaterialState.focused].
/// * [MaterialState.pressed].
OutlinedBorder? get shape;
/// {@macro flutter.widgets.Clip}
///
......@@ -576,6 +605,7 @@ class Chip extends StatelessWidget implements ChipAttributes, DeletableChipAttri
this.deleteIconColor,
this.useDeleteButtonTooltip = true,
this.deleteButtonTooltipMessage,
this.side,
this.shape,
this.clipBehavior = Clip.none,
this.focusNode,
......@@ -602,7 +632,9 @@ class Chip extends StatelessWidget implements ChipAttributes, DeletableChipAttri
@override
final EdgeInsetsGeometry? labelPadding;
@override
final ShapeBorder? shape;
final BorderSide? side;
@override
final OutlinedBorder? shape;
@override
final Clip clipBehavior;
@override
......@@ -646,6 +678,7 @@ class Chip extends StatelessWidget implements ChipAttributes, DeletableChipAttri
useDeleteButtonTooltip: useDeleteButtonTooltip,
deleteButtonTooltipMessage: deleteButtonTooltipMessage,
tapEnabled: false,
side: side,
shape: shape,
clipBehavior: clipBehavior,
focusNode: focusNode,
......@@ -742,6 +775,7 @@ class InputChip extends StatelessWidget
this.disabledColor,
this.selectedColor,
this.tooltip,
this.side,
this.shape,
this.clipBehavior = Clip.none,
this.focusNode,
......@@ -801,7 +835,9 @@ class InputChip extends StatelessWidget
@override
final String? tooltip;
@override
final ShapeBorder? shape;
final BorderSide? side;
@override
final OutlinedBorder? shape;
@override
final Clip clipBehavior;
@override
......@@ -850,6 +886,7 @@ class InputChip extends StatelessWidget
disabledColor: disabledColor,
selectedColor: selectedColor,
tooltip: tooltip,
side: side,
shape: shape,
clipBehavior: clipBehavior,
focusNode: focusNode,
......@@ -945,6 +982,7 @@ class ChoiceChip extends StatelessWidget
this.selectedColor,
this.disabledColor,
this.tooltip,
this.side,
this.shape,
this.clipBehavior = Clip.none,
this.focusNode,
......@@ -986,7 +1024,9 @@ class ChoiceChip extends StatelessWidget
@override
final String? tooltip;
@override
final ShapeBorder? shape;
final BorderSide? side;
@override
final OutlinedBorder? shape;
@override
final Clip clipBehavior;
@override
......@@ -1028,6 +1068,7 @@ class ChoiceChip extends StatelessWidget
showCheckmark: false,
onDeleted: null,
tooltip: tooltip,
side: side,
shape: shape,
clipBehavior: clipBehavior,
focusNode: focusNode,
......@@ -1156,6 +1197,7 @@ class FilterChip extends StatelessWidget
this.disabledColor,
this.selectedColor,
this.tooltip,
this.side,
this.shape,
this.clipBehavior = Clip.none,
this.focusNode,
......@@ -1199,7 +1241,9 @@ class FilterChip extends StatelessWidget
@override
final String? tooltip;
@override
final ShapeBorder? shape;
final BorderSide? side;
@override
final OutlinedBorder? shape;
@override
final Clip clipBehavior;
@override
......@@ -1242,6 +1286,7 @@ class FilterChip extends StatelessWidget
pressElevation: pressElevation,
selected: selected,
tooltip: tooltip,
side: side,
shape: shape,
clipBehavior: clipBehavior,
focusNode: focusNode,
......@@ -1325,6 +1370,7 @@ class ActionChip extends StatelessWidget implements ChipAttributes, TappableChip
required this.onPressed,
this.pressElevation,
this.tooltip,
this.side,
this.shape,
this.clipBehavior = Clip.none,
this.focusNode,
......@@ -1362,7 +1408,9 @@ class ActionChip extends StatelessWidget implements ChipAttributes, TappableChip
@override
final String? tooltip;
@override
final ShapeBorder? shape;
final BorderSide? side;
@override
final OutlinedBorder? shape;
@override
final Clip clipBehavior;
@override
......@@ -1393,6 +1441,7 @@ class ActionChip extends StatelessWidget implements ChipAttributes, TappableChip
tooltip: tooltip,
labelStyle: labelStyle,
backgroundColor: backgroundColor,
side: side,
shape: shape,
clipBehavior: clipBehavior,
focusNode: focusNode,
......@@ -1477,6 +1526,7 @@ class RawChip extends StatefulWidget
this.disabledColor,
this.selectedColor,
this.tooltip,
this.side,
this.shape,
this.clipBehavior = Clip.none,
this.focusNode,
......@@ -1535,7 +1585,9 @@ class RawChip extends StatefulWidget
@override
final String? tooltip;
@override
final ShapeBorder? shape;
final BorderSide? side;
@override
final OutlinedBorder? shape;
@override
final Clip clipBehavior;
@override
......@@ -1731,6 +1783,15 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
});
}
OutlinedBorder _getShape(ChipThemeData theme) {
final BorderSide? resolvedSide = MaterialStateProperty.resolveAs<BorderSide?>(widget.side, _states)
?? MaterialStateProperty.resolveAs<BorderSide?>(theme.side, _states);
final OutlinedBorder resolvedShape = MaterialStateProperty.resolveAs<OutlinedBorder?>(widget.shape, _states)
?? MaterialStateProperty.resolveAs<OutlinedBorder?>(theme.shape, _states)
?? const StadiumBorder();
return resolvedShape.copyWith(side: resolvedSide);
}
/// Picks between three different colors, depending upon the state of two
/// different animations.
Color? getBackgroundColor(ChipThemeData theme) {
......@@ -1860,7 +1921,7 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
final ThemeData theme = Theme.of(context)!;
final ChipThemeData chipTheme = ChipTheme.of(context);
final TextDirection? textDirection = Directionality.maybeOf(context);
final ShapeBorder shape = widget.shape ?? chipTheme.shape;
final OutlinedBorder resolvedShape = _getShape(chipTheme);
final double elevation = widget.elevation ?? chipTheme.elevation ?? _defaultElevation;
final double pressElevation = widget.pressElevation ?? chipTheme.pressElevation ?? _defaultPressElevation;
final Color shadowColor = widget.shadowColor ?? chipTheme.shadowColor ?? _defaultShadowColor;
......@@ -1869,7 +1930,7 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
final bool showCheckmark = widget.showCheckmark ?? chipTheme.showCheckmark ?? true;
final TextStyle effectiveLabelStyle = widget.labelStyle ?? chipTheme.labelStyle;
final Color? resolvedLabelColor = MaterialStateProperty.resolveAs<Color?>(effectiveLabelStyle.color, _states);
final Color? resolvedLabelColor = MaterialStateProperty.resolveAs<Color?>(effectiveLabelStyle.color, _states);
final TextStyle resolvedLabelStyle = effectiveLabelStyle.copyWith(color: resolvedLabelColor);
final EdgeInsetsGeometry labelPadding = widget.labelPadding ?? chipTheme.labelPadding ?? _defaultLabelPadding;
......@@ -1877,7 +1938,7 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
elevation: isTapping ? pressElevation : elevation,
shadowColor: widget.selected ? selectedShadowColor : shadowColor,
animationDuration: pressedAnimationDuration,
shape: shape,
shape: resolvedShape,
clipBehavior: widget.clipBehavior,
child: InkWell(
onFocusChange: _handleFocus,
......@@ -1893,13 +1954,13 @@ class _RawChipState extends State<RawChip> with TickerProviderStateMixin<RawChip
context,
deleteIconKey,
),
customBorder: shape,
customBorder: resolvedShape,
child: AnimatedBuilder(
animation: Listenable.merge(<Listenable>[selectController, enableController]),
builder: (BuildContext context, Widget? child) {
return Container(
decoration: ShapeDecoration(
shape: shape,
shape: resolvedShape,
color: getBackgroundColor(chipTheme),
),
child: child,
......
......@@ -187,7 +187,8 @@ class ChipThemeData with Diagnosticable {
this.checkmarkColor,
this.labelPadding,
required this.padding,
required this.shape,
this.side,
this.shape,
required this.labelStyle,
required this.secondaryLabelStyle,
required this.brightness,
......@@ -198,7 +199,6 @@ class ChipThemeData with Diagnosticable {
assert(selectedColor != null),
assert(secondarySelectedColor != null),
assert(padding != null),
assert(shape != null),
assert(labelStyle != null),
assert(secondaryLabelStyle != null),
assert(brightness != null);
......@@ -244,7 +244,6 @@ class ChipThemeData with Diagnosticable {
const int disabledAlpha = 0x0c; // 38% * 12% = 5%
const int selectAlpha = 0x3d; // 12% + 12% = 24%
const int textLabelAlpha = 0xde; // 87%
const ShapeBorder shape = StadiumBorder();
const EdgeInsetsGeometry padding = EdgeInsets.all(4.0);
primaryColor = primaryColor ?? (brightness == Brightness.light ? Colors.black : Colors.white);
......@@ -265,7 +264,6 @@ class ChipThemeData with Diagnosticable {
selectedColor: selectedColor,
secondarySelectedColor: secondarySelectedColor,
padding: padding,
shape: shape,
labelStyle: labelStyle,
secondaryLabelStyle: secondaryLabelStyle,
brightness: brightness!,
......@@ -350,10 +348,37 @@ class ChipThemeData with Diagnosticable {
/// Defaults to 4 logical pixels on all sides.
final EdgeInsetsGeometry padding;
/// The color and weight of the chip's outline.
///
/// If null, the chip defaults to the border side of [shape].
///
/// This value is combined with [shape] to create a shape decorated with an
/// outline. If it is a [MaterialStateBorderSide],
/// [MaterialStateProperty.resolve] is used for the following
/// [MaterialState]s:
///
/// * [MaterialState.disabled].
/// * [MaterialState.selected].
/// * [MaterialState.hovered].
/// * [MaterialState.focused].
/// * [MaterialState.pressed].
final BorderSide? side;
/// The border to draw around the chip.
///
/// Defaults to a [StadiumBorder]. Must not be null.
final ShapeBorder shape;
/// If null, the chip defaults to a [StadiumBorder].
///
/// This shape is combined with [side] to create a shape decorated with an
/// outline. If it is a [MaterialStateOutlinedBorder],
/// [MaterialStateProperty.resolve] is used for the following
/// [MaterialState]s:
///
/// * [MaterialState.disabled].
/// * [MaterialState.selected].
/// * [MaterialState.hovered].
/// * [MaterialState.focused].
/// * [MaterialState.pressed].
final OutlinedBorder? shape;
/// The style to be applied to the chip's label.
///
......@@ -396,7 +421,8 @@ class ChipThemeData with Diagnosticable {
Color? checkmarkColor,
EdgeInsetsGeometry? labelPadding,
EdgeInsetsGeometry? padding,
ShapeBorder? shape,
BorderSide? side,
OutlinedBorder? shape,
TextStyle? labelStyle,
TextStyle? secondaryLabelStyle,
Brightness? brightness,
......@@ -414,6 +440,7 @@ class ChipThemeData with Diagnosticable {
checkmarkColor: checkmarkColor ?? this.checkmarkColor,
labelPadding: labelPadding ?? this.labelPadding,
padding: padding ?? this.padding,
side: side ?? this.side,
shape: shape ?? this.shape,
labelStyle: labelStyle ?? this.labelStyle,
secondaryLabelStyle: secondaryLabelStyle ?? this.secondaryLabelStyle,
......@@ -443,7 +470,8 @@ class ChipThemeData with Diagnosticable {
checkmarkColor: Color.lerp(a?.checkmarkColor, b?.checkmarkColor, t),
labelPadding: EdgeInsetsGeometry.lerp(a?.labelPadding, b?.labelPadding, t),
padding: EdgeInsetsGeometry.lerp(a?.padding, b?.padding, t)!,
shape: ShapeBorder.lerp(a?.shape, b?.shape, t)!,
side: _lerpSides(a?.side, b?.side, t),
shape: _lerpShapes(a?.shape, b?.shape, t),
labelStyle: TextStyle.lerp(a?.labelStyle, b?.labelStyle, t)!,
secondaryLabelStyle: TextStyle.lerp(a?.secondaryLabelStyle, b?.secondaryLabelStyle, t)!,
brightness: t < 0.5 ? a?.brightness ?? Brightness.light : b?.brightness ?? Brightness.light,
......@@ -452,6 +480,24 @@ class ChipThemeData with Diagnosticable {
);
}
// Special case because BorderSide.lerp() doesn't support null arguments.
static BorderSide? _lerpSides(BorderSide? a, BorderSide? b, double t) {
if (a == null && b == null)
return null;
if (a == null)
return BorderSide.lerp(BorderSide(width: 0, color: b!.color.withAlpha(0)), b, t);
if (b == null)
return BorderSide.lerp(BorderSide(width: 0, color: a.color.withAlpha(0)), a, t);
return BorderSide.lerp(a, b, t);
}
// TODO(perclasson): OutlinedBorder needs a lerp method - https://github.com/flutter/flutter/issues/60555.
static OutlinedBorder? _lerpShapes(OutlinedBorder? a, OutlinedBorder? b, double t) {
if (a == null && b == null)
return null;
return ShapeBorder.lerp(a, b, t) as OutlinedBorder?;
}
@override
int get hashCode {
return hashValues(
......@@ -465,6 +511,7 @@ class ChipThemeData with Diagnosticable {
checkmarkColor,
labelPadding,
padding,
side,
shape,
labelStyle,
secondaryLabelStyle,
......@@ -493,6 +540,7 @@ class ChipThemeData with Diagnosticable {
&& other.checkmarkColor == checkmarkColor
&& other.labelPadding == labelPadding
&& other.padding == padding
&& other.side == side
&& other.shape == shape
&& other.labelStyle == labelStyle
&& other.secondaryLabelStyle == secondaryLabelStyle
......@@ -520,6 +568,7 @@ class ChipThemeData with Diagnosticable {
properties.add(ColorProperty('checkMarkColor', checkmarkColor, defaultValue: defaultData.checkmarkColor));
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('labelPadding', labelPadding, defaultValue: defaultData.labelPadding));
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: defaultData.padding));
properties.add(DiagnosticsProperty<BorderSide>('side', side, defaultValue: defaultData.side));
properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: defaultData.shape));
properties.add(DiagnosticsProperty<TextStyle>('labelStyle', labelStyle, defaultValue: defaultData.labelStyle));
properties.add(DiagnosticsProperty<TextStyle>('secondaryLabelStyle', secondaryLabelStyle, defaultValue: defaultData.secondaryLabelStyle));
......
......@@ -21,9 +21,15 @@ import 'package:flutter/rendering.dart';
/// * [MaterialStateColor], a [Color] that implements `MaterialStateProperty`
/// which is used in APIs that need to accept either a [Color] or a
/// `MaterialStateProperty<Color>`.
/// * [MaterialStateMouseCursor], a [MouseCursor] that implements `MaterialStateProperty`
/// which is used in APIs that need to accept either a [MouseCursor] or a
/// [MaterialStateProperty<MouseCursor>].
/// * [MaterialStateMouseCursor], a [MouseCursor] that implements
/// `MaterialStateProperty` which is used in APIs that need to accept either
/// a [MouseCursor] or a [MaterialStateProperty<MouseCursor>].
/// * [MaterialStateOutlinedBorder], an [OutlinedBorder] that implements
/// `MaterialStateProperty` which is used in APIs that need to accept either
/// an [OutlinedBorder] or a [MaterialStateProperty<OutlinedBorder>].
/// * [MaterialStateBorderSide], a [BorderSide] that implements
/// `MaterialStateProperty` which is used in APIs that need to accept either
/// a [BorderSide] or a [MaterialStateProperty<BorderSide>].
enum MaterialState {
/// The state when the user drags their mouse cursor over the given widget.
......@@ -282,6 +288,123 @@ class _EnabledAndDisabledMouseCursor extends MaterialStateMouseCursor {
String get debugDescription => 'MaterialStateMouseCursor($name)';
}
/// Defines a [BorderSide] whose value depends on a set of [MaterialState]s
/// which represent the interactive state of a component.
///
/// To use a [MaterialStateBorderSide], you should create a subclass of a
/// [MaterialStateBorderSide] and override the abstract `resolve` method.
///
/// {@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 BorderSide(
/// width: 1,
/// color: Colors.red,
/// );
/// }
/// return null; // Defer to default value on the theme or widget.
/// }
/// }
/// ```
///
/// ```dart
/// bool isSelected = true;
///
/// Widget build(BuildContext context) {
/// return FilterChip(
/// label: Text('Select chip'),
/// selected: isSelected,
/// onSelected: (bool value) {
/// setState(() {
/// isSelected = value;
/// });
/// },
/// side: RedSelectedBorderSide(),
/// );
/// }
/// ```
/// {@end-tool}
///
/// This class should only be used for parameters which are documented to take
/// [MaterialStateBorderSide], otherwise only the default state will be used.
abstract class MaterialStateBorderSide extends BorderSide implements MaterialStateProperty<BorderSide?> {
/// Creates a [MaterialStateBorderSide].
const MaterialStateBorderSide();
/// Returns a [BorderSide] that's to be used when a Material component is
/// in the specified state. Return null to defer to the default value of the
/// widget or theme.
@override
BorderSide? resolve(Set<MaterialState> states);
}
/// Defines an [OutlinedBorder] whose value depends on a set of [MaterialState]s
/// which represent the interactive state of a component.
///
/// To use a [MaterialStateOutlinedBorder], you should create a subclass of an
/// [OutlinedBorder] and implement [MaterialStateOutlinedBorder]'s abstract
/// `resolve` method.
///
/// {@tool dartpad --template=stateful_widget_material}
///
/// This example defines a subclass of [RoundedRectangleBorder] and an
/// implementation of [MaterialStateOutlinedBorder], that resolves to
/// [RoundedRectangleBorder] when its widget is selected.
///
/// ```dart preamble
/// class SelectedBorder extends RoundedRectangleBorder implements MaterialStateOutlinedBorder {
/// @override
/// OutlinedBorder resolve(Set<MaterialState> states) {
/// if (states.contains(MaterialState.selected)) {
/// return RoundedRectangleBorder();
/// }
/// return null; // Defer to default value on the theme or widget.
/// }
/// }
/// ```
///
/// ```dart
/// bool isSelected = true;
///
/// Widget build(BuildContext context) {
/// return FilterChip(
/// label: Text('Select chip'),
/// selected: isSelected,
/// onSelected: (bool value) {
/// setState(() {
/// isSelected = value;
/// });
/// },
/// shape: SelectedBorder(),
/// );
/// }
/// ```
/// {@end-tool}
///
/// This class should only be used for parameters which are documented to take
/// [MaterialStateOutlinedBorder], otherwise only the default state will be used.
///
/// See also:
///
/// * [ShapeBorder] the base class for shape outlines.
abstract class MaterialStateOutlinedBorder extends OutlinedBorder implements MaterialStateProperty<OutlinedBorder?> {
/// Creates a [MaterialStateOutlinedBorder].
const MaterialStateOutlinedBorder();
/// Returns an [OutlinedBorder] that's to be used when a Material component is
/// in the specified state. Return null to defer to the default value of the
/// widget or theme.
@override
OutlinedBorder? resolve(Set<MaterialState> states);
}
/// Interface for classes that [resolve] to a value of type `T` based
/// on a widget's interactive "state", which is defined as a set
/// of [MaterialState]s.
......
......@@ -2382,6 +2382,216 @@ void main() {
await gesture.removePointer();
});
testWidgets('Chip uses stateful border side color in different states', (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(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 shape in different states', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode();
OutlinedBorder? getShape(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled))
return const BeveledRectangleBorder();
else if (states.contains(MaterialState.pressed))
return const CircleBorder();
else if (states.contains(MaterialState.hovered))
return const ContinuousRectangleBorder();
else if (states.contains(MaterialState.focused))
return const RoundedRectangleBorder();
else if (states.contains(MaterialState.selected))
return const BeveledRectangleBorder();
return null;
}
Widget chipWidget({ bool enabled = true, bool selected = false }) {
return MaterialApp(
home: Scaffold(
body: Focus(
focusNode: focusNode,
child: ChoiceChip(
selected: selected,
label: const Text('Chip'),
shape: _MaterialStateOutlinedBorder(getShape),
onSelected: enabled ? (_) {} : null,
),
),
),
);
}
// Default, not disabled. Defers to default shape.
await tester.pumpWidget(chipWidget());
expect(getMaterial(tester).shape, isA<StadiumBorder>());
// Selected.
await tester.pumpWidget(chipWidget(selected: true));
expect(getMaterial(tester).shape, isA<BeveledRectangleBorder>());
// Focused.
final FocusNode chipFocusNode = focusNode.children.first;
chipFocusNode.requestFocus();
await tester.pumpAndSettle();
expect(getMaterial(tester).shape, isA<RoundedRectangleBorder>());
// 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(getMaterial(tester).shape, isA<ContinuousRectangleBorder>());
// Pressed.
await gesture.down(center);
await tester.pumpAndSettle();
expect(getMaterial(tester).shape, isA<CircleBorder>());
// Disabled.
await tester.pumpWidget(chipWidget(enabled: false));
await tester.pumpAndSettle();
expect(getMaterial(tester).shape, isA<BeveledRectangleBorder>());
// Teardown.
await gesture.removePointer();
});
testWidgets('Chip defers to theme, if shape and side resolves to null', (WidgetTester tester) async {
const OutlinedBorder themeShape = StadiumBorder();
const OutlinedBorder selectedShape = RoundedRectangleBorder();
const BorderSide themeBorderSide = BorderSide(color: Color(0x00000001), width: 1);
const BorderSide selectedBorderSide = BorderSide(color: Color(0x00000002), width: 1);
OutlinedBorder? getShape(Set<MaterialState> states) {
if (states.contains(MaterialState.selected))
return selectedShape;
return null;
}
BorderSide? getBorderSide(Set<MaterialState> states) {
if (states.contains(MaterialState.selected))
return selectedBorderSide;
return null;
}
Widget chipWidget({ bool enabled = true, bool selected = false }) {
return MaterialApp(
theme: ThemeData(
chipTheme: ThemeData.light().chipTheme.copyWith(
shape: themeShape,
side: themeBorderSide,
),
),
home: Scaffold(
body: ChoiceChip(
selected: selected,
label: const Text('Chip'),
shape: _MaterialStateOutlinedBorder(getShape),
side: _MaterialStateBorderSide(getBorderSide),
onSelected: enabled ? (_) {} : null,
),
),
);
}
// Default, not disabled. Defer to theme.
await tester.pumpWidget(chipWidget());
expect(getMaterial(tester).shape, isA<StadiumBorder>());
expect(find.byType(RawChip), paints..rrect(color: themeBorderSide.color));
// Selected.
await tester.pumpWidget(chipWidget(selected: true));
expect(getMaterial(tester).shape, isA<RoundedRectangleBorder>());
expect(find.byType(RawChip), paints..drrect(color: selectedBorderSide.color));
});
testWidgets('loses focus when disabled', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'InputChip');
await tester.pumpWidget(
......@@ -2707,3 +2917,21 @@ void main() {
await tapGesture.up();
});
}
class _MaterialStateOutlinedBorder extends StadiumBorder implements MaterialStateOutlinedBorder {
const _MaterialStateOutlinedBorder(this.resolver);
final MaterialPropertyResolver<OutlinedBorder?> resolver;
@override
OutlinedBorder? resolve(Set<MaterialState> states) => resolver(states);
}
class _MaterialStateBorderSide extends MaterialStateBorderSide {
const _MaterialStateBorderSide(this.resolver);
final MaterialPropertyResolver<BorderSide?> resolver;
@override
BorderSide? resolve(Set<MaterialState> states) => resolver(states);
}
......@@ -184,7 +184,8 @@ void main() {
expect(lightTheme.secondarySelectedColor, equals(customColor1.withAlpha(0x3d)));
expect(lightTheme.labelPadding, isNull);
expect(lightTheme.padding, equals(const EdgeInsets.all(4.0)));
expect(lightTheme.shape, isA<StadiumBorder>());
expect(lightTheme.side, isNull);
expect(lightTheme.shape, isNull);
expect(lightTheme.labelStyle.color, equals(Colors.black.withAlpha(0xde)));
expect(lightTheme.secondaryLabelStyle.color, equals(customColor1.withAlpha(0xde)));
expect(lightTheme.brightness, equals(Brightness.light));
......@@ -202,7 +203,8 @@ void main() {
expect(darkTheme.secondarySelectedColor, equals(customColor1.withAlpha(0x3d)));
expect(darkTheme.labelPadding, isNull);
expect(darkTheme.padding, equals(const EdgeInsets.all(4.0)));
expect(darkTheme.shape, isA<StadiumBorder>());
expect(darkTheme.side, isNull);
expect(darkTheme.shape, isNull);
expect(darkTheme.labelStyle.color, equals(Colors.white.withAlpha(0xde)));
expect(darkTheme.secondaryLabelStyle.color, equals(customColor1.withAlpha(0xde)));
expect(darkTheme.brightness, equals(Brightness.dark));
......@@ -220,7 +222,8 @@ void main() {
expect(customTheme.secondarySelectedColor, equals(customColor2.withAlpha(0x3d)));
expect(customTheme.labelPadding, isNull);
expect(customTheme.padding, equals(const EdgeInsets.all(4.0)));
expect(customTheme.shape, isA<StadiumBorder>());
expect(customTheme.side, isNull);
expect(customTheme.shape, isNull);
expect(customTheme.labelStyle.color, equals(customColor1.withAlpha(0xde)));
expect(customTheme.secondaryLabelStyle.color, equals(customColor2.withAlpha(0xde)));
expect(customTheme.brightness, equals(Brightness.light));
......@@ -234,6 +237,8 @@ void main() {
).copyWith(
elevation: 1.0,
labelPadding: const EdgeInsets.symmetric(horizontal: 8.0),
shape: const StadiumBorder(),
side: const BorderSide(color: Colors.black),
pressElevation: 4.0,
shadowColor: Colors.black,
selectedShadowColor: Colors.black,
......@@ -246,6 +251,8 @@ void main() {
).copyWith(
padding: const EdgeInsets.all(2.0),
labelPadding: const EdgeInsets.only(top: 8.0, bottom: 8.0),
shape: const BeveledRectangleBorder(),
side: const BorderSide(color: Colors.white),
elevation: 5.0,
pressElevation: 10.0,
shadowColor: Colors.white,
......@@ -264,7 +271,8 @@ void main() {
expect(lerp.selectedShadowColor, equals(middleGrey));
expect(lerp.labelPadding, equals(const EdgeInsets.all(4.0)));
expect(lerp.padding, equals(const EdgeInsets.all(3.0)));
expect(lerp.shape, isA<StadiumBorder>());
expect(lerp.side!.color, equals(middleGrey));
expect(lerp.shape, isA<BeveledRectangleBorder>());
expect(lerp.labelStyle.color, equals(middleGrey.withAlpha(0xde)));
expect(lerp.secondaryLabelStyle.color, equals(middleGrey.withAlpha(0xde)));
expect(lerp.brightness, equals(Brightness.light));
......@@ -284,7 +292,8 @@ void main() {
expect(lerpANull25.selectedShadowColor, equals(Colors.white.withAlpha(0x40)));
expect(lerpANull25.labelPadding, equals(const EdgeInsets.only(left: 0.0, top: 2.0, right: 0.0, bottom: 2.0)));
expect(lerpANull25.padding, equals(const EdgeInsets.all(0.5)));
expect(lerpANull25.shape, isA<StadiumBorder>());
expect(lerpANull25.side!.color, equals(Colors.white.withAlpha(0x3f)));
expect(lerpANull25.shape, isA<BeveledRectangleBorder>());
expect(lerpANull25.labelStyle.color, equals(Colors.black.withAlpha(0x38)));
expect(lerpANull25.secondaryLabelStyle.color, equals(Colors.white.withAlpha(0x38)));
expect(lerpANull25.brightness, equals(Brightness.light));
......@@ -302,7 +311,8 @@ void main() {
expect(lerpANull75.selectedShadowColor, equals(Colors.white.withAlpha(0xbf)));
expect(lerpANull75.labelPadding, equals(const EdgeInsets.only(left: 0.0, top: 6.0, right: 0.0, bottom: 6.0)));
expect(lerpANull75.padding, equals(const EdgeInsets.all(1.5)));
expect(lerpANull75.shape, isA<StadiumBorder>());
expect(lerpANull75.side!.color, equals(Colors.white.withAlpha(0xbf)));
expect(lerpANull75.shape, isA<BeveledRectangleBorder>());
expect(lerpANull75.labelStyle.color, equals(Colors.black.withAlpha(0xa7)));
expect(lerpANull75.secondaryLabelStyle.color, equals(Colors.white.withAlpha(0xa7)));
expect(lerpANull75.brightness, equals(Brightness.light));
......@@ -320,6 +330,7 @@ void main() {
expect(lerpBNull25.selectedShadowColor, equals(Colors.black.withAlpha(0xbf)));
expect(lerpBNull25.labelPadding, equals(const EdgeInsets.only(left: 6.0, top: 0.0, right: 6.0, bottom: 0.0)));
expect(lerpBNull25.padding, equals(const EdgeInsets.all(3.0)));
expect(lerpBNull25.side!.color, equals(Colors.black.withAlpha(0x3f)));
expect(lerpBNull25.shape, isA<StadiumBorder>());
expect(lerpBNull25.labelStyle.color, equals(Colors.white.withAlpha(0xa7)));
expect(lerpBNull25.secondaryLabelStyle.color, equals(Colors.black.withAlpha(0xa7)));
......@@ -338,6 +349,7 @@ void main() {
expect(lerpBNull75.selectedShadowColor, equals(Colors.black.withAlpha(0x40)));
expect(lerpBNull75.labelPadding, equals(const EdgeInsets.only(left: 2.0, top: 0.0, right: 2.0, bottom: 0.0)));
expect(lerpBNull75.padding, equals(const EdgeInsets.all(1.0)));
expect(lerpBNull75.side!.color, equals(Colors.black.withAlpha(0xbf)));
expect(lerpBNull75.shape, isA<StadiumBorder>());
expect(lerpBNull75.labelStyle.color, equals(Colors.white.withAlpha(0x38)));
expect(lerpBNull75.secondaryLabelStyle.color, equals(Colors.black.withAlpha(0x38)));
......@@ -440,4 +452,95 @@ void main() {
// Teardown.
await gesture.removePointer();
});
testWidgets('Chip uses stateful border side from chip theme', (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(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 shape from chip theme', (WidgetTester tester) async {
OutlinedBorder? getShape(Set<MaterialState> states) {
if (states.contains(MaterialState.selected))
return const RoundedRectangleBorder();
return null;
}
Widget chipWidget({ bool selected = false }) {
return MaterialApp(
theme: ThemeData(
chipTheme: ThemeData.light().chipTheme.copyWith(
shape: _MaterialStateOutlinedBorder(getShape),
),
),
home: Scaffold(
body: ChoiceChip(
label: const Text('Chip'),
selected: selected,
onSelected: (_) {},
),
),
);
}
// Default.
await tester.pumpWidget(chipWidget());
expect(getMaterial(tester).shape, isA<StadiumBorder>());
// Selected.
await tester.pumpWidget(chipWidget(selected: true));
expect(getMaterial(tester).shape, isA<RoundedRectangleBorder>());
});
}
class _MaterialStateOutlinedBorder extends StadiumBorder implements MaterialStateOutlinedBorder {
const _MaterialStateOutlinedBorder(this.resolver);
final MaterialPropertyResolver<OutlinedBorder?> resolver;
@override
OutlinedBorder? resolve(Set<MaterialState> states) => resolver(states);
}
class _MaterialStateBorderSide extends MaterialStateBorderSide {
const _MaterialStateBorderSide(this.resolver);
final MaterialPropertyResolver<BorderSide?> resolver;
@override
BorderSide? resolve(Set<MaterialState> states) => resolver(states);
}
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