Unverified Commit 91dc513a authored by Qun Cheng's avatar Qun Cheng Committed by GitHub

Add missing parameters to `CheckboxListTile` (#120118)

* Add missing parameters to CheckboxListTile

* Update test message and api doc

* Reorder parameters

---------
Co-authored-by: 's avatarQun Cheng <quncheng@google.com>
parent c8c86214
......@@ -254,10 +254,12 @@ class Checkbox extends StatefulWidget {
/// [ThemeData.focusColor] is used.
final Color? focusColor;
/// {@template flutter.material.checkbox.hoverColor}
/// The color for the checkbox's [Material] when a pointer is hovering over it.
///
/// If [overlayColor] returns a non-null color in the [MaterialState.hovered]
/// state, it will be used instead.
/// {@endtemplate}
///
/// If null, then the value of [CheckboxThemeData.overlayColor] is used in the
/// hovered state. If that is also null, then the value of
......@@ -332,10 +334,12 @@ class Checkbox extends StatefulWidget {
/// will be width 2.
final BorderSide? side;
/// {@template flutter.material.checkbox.isError}
/// True if this checkbox wants to show an error state.
///
/// The checkbox will have different default container color and check color when
/// this is true. This is only used when [ThemeData.useMaterial3] is set to true.
/// {@endtemplate}
///
/// Must not be null. Defaults to false.
final bool isError;
......
......@@ -163,8 +163,20 @@ class CheckboxListTile extends StatelessWidget {
super.key,
required this.value,
required this.onChanged,
this.mouseCursor,
this.activeColor,
this.fillColor,
this.checkColor,
this.hoverColor,
this.overlayColor,
this.splashRadius,
this.materialTapTargetSize,
this.visualDensity,
this.focusNode,
this.autofocus = false,
this.shape,
this.side,
this.isError = false,
this.enabled,
this.tileColor,
this.title,
......@@ -174,15 +186,10 @@ class CheckboxListTile extends StatelessWidget {
this.secondary,
this.selected = false,
this.controlAffinity = ListTileControlAffinity.platform,
this.autofocus = false,
this.contentPadding,
this.tristate = false,
this.shape,
this.checkboxShape,
this.selectedTileColor,
this.side,
this.visualDensity,
this.focusNode,
this.onFocusChange,
this.enableFeedback,
}) : assert(tristate || value != null),
......@@ -219,16 +226,98 @@ class CheckboxListTile extends StatelessWidget {
/// {@end-tool}
final ValueChanged<bool?>? onChanged;
/// The cursor for a mouse pointer when it enters or is hovering over the
/// widget.
///
/// If [mouseCursor] is a [MaterialStateProperty<MouseCursor>],
/// [MaterialStateProperty.resolve] is used for the following [MaterialState]s:
///
/// * [MaterialState.selected].
/// * [MaterialState.hovered].
/// * [MaterialState.disabled].
///
/// If null, then the value of [CheckboxThemeData.mouseCursor] is used. If
/// that is also null, then [MaterialStateMouseCursor.clickable] is used.
final MouseCursor? mouseCursor;
/// The color to use when this checkbox is checked.
///
/// Defaults to [ColorScheme.secondary] of the current [Theme].
final Color? activeColor;
/// The color that fills the checkbox.
///
/// Resolves in the following states:
/// * [MaterialState.selected].
/// * [MaterialState.hovered].
/// * [MaterialState.disabled].
///
/// If null, then the value of [activeColor] is used in the selected
/// state. If that is also null, the value of [CheckboxThemeData.fillColor]
/// is used. If that is also null, then the default value is used.
final MaterialStateProperty<Color?>? fillColor;
/// The color to use for the check icon when this checkbox is checked.
///
/// Defaults to Color(0xFFFFFFFF).
final Color? checkColor;
/// {@macro flutter.material.checkbox.hoverColor}
final Color? hoverColor;
/// The color for the checkbox's [Material].
///
/// Resolves in the following states:
/// * [MaterialState.pressed].
/// * [MaterialState.selected].
/// * [MaterialState.hovered].
///
/// If null, then the value of [activeColor] with alpha [kRadialReactionAlpha]
/// and [hoverColor] is used in the pressed and hovered state. If that is also null,
/// the value of [CheckboxThemeData.overlayColor] is used. If that is also null,
/// then the the default value is used in the pressed and hovered state.
final MaterialStateProperty<Color?>? overlayColor;
/// {@macro flutter.material.checkbox.splashRadius}
///
/// If null, then the value of [CheckboxThemeData.splashRadius] is used. If
/// that is also null, then [kRadialReactionRadius] is used.
final double? splashRadius;
/// {@macro flutter.material.checkbox.materialTapTargetSize}
///
/// Defaults to [MaterialTapTargetSize.shrinkWrap].
final MaterialTapTargetSize? materialTapTargetSize;
/// Defines how compact the list tile's layout will be.
///
/// {@macro flutter.material.themedata.visualDensity}
final VisualDensity? visualDensity;
/// {@macro flutter.widgets.Focus.focusNode}
final FocusNode? focusNode;
/// {@macro flutter.widgets.Focus.autofocus}
final bool autofocus;
/// {@macro flutter.material.ListTile.shape}
final ShapeBorder? shape;
/// {@macro flutter.material.checkbox.side}
///
/// The given value is passed directly to [Checkbox.side].
///
/// If this property is null, then [CheckboxThemeData.side] of
/// [ThemeData.checkboxTheme] is used. If that is also null, then the side
/// will be width 2.
final BorderSide? side;
/// {@macro flutter.material.checkbox.isError}
///
/// Defaults to false.
final bool isError;
/// {@macro flutter.material.ListTile.tileColor}
final Color? tileColor;
......@@ -270,9 +359,6 @@ class CheckboxListTile extends StatelessWidget {
/// Where to place the control relative to the text.
final ListTileControlAffinity controlAffinity;
/// {@macro flutter.widgets.Focus.autofocus}
final bool autofocus;
/// Defines insets surrounding the tile's contents.
///
/// This value will surround the [Checkbox], [title], [subtitle], and [secondary]
......@@ -293,9 +379,6 @@ class CheckboxListTile extends StatelessWidget {
/// If tristate is false (the default), [value] must not be null.
final bool tristate;
/// {@macro flutter.material.ListTile.shape}
final ShapeBorder? shape;
/// {@macro flutter.material.checkbox.shape}
///
/// If this property is null then [CheckboxThemeData.shape] of [ThemeData.checkboxTheme]
......@@ -306,23 +389,6 @@ class CheckboxListTile extends StatelessWidget {
/// If non-null, defines the background color when [CheckboxListTile.selected] is true.
final Color? selectedTileColor;
/// {@macro flutter.material.checkbox.side}
///
/// The given value is passed directly to [Checkbox.side].
///
/// If this property is null, then [CheckboxThemeData.side] of
/// [ThemeData.checkboxTheme] is used. If that is also null, then the side
/// will be width 2.
final BorderSide? side;
/// Defines how compact the list tile's layout will be.
///
/// {@macro flutter.material.themedata.visualDensity}
final VisualDensity? visualDensity;
/// {@macro flutter.widgets.Focus.focusNode}
final FocusNode? focusNode;
/// {@macro flutter.material.inkwell.onFocusChange}
final ValueChanged<bool>? onFocusChange;
......@@ -359,14 +425,20 @@ class CheckboxListTile extends StatelessWidget {
Widget build(BuildContext context) {
final Widget control = Checkbox(
value: value,
onChanged: enabled ?? true ? onChanged : null ,
onChanged: enabled ?? true ? onChanged : null,
mouseCursor: mouseCursor,
activeColor: activeColor,
fillColor: fillColor,
checkColor: checkColor,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
hoverColor: hoverColor,
overlayColor: overlayColor,
splashRadius: splashRadius,
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
autofocus: autofocus,
tristate: tristate,
shape: checkboxShape,
side: side,
isError: isError,
);
Widget? leading, trailing;
switch (controlAffinity) {
......
......@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
......@@ -501,6 +502,425 @@ void main() {
expect(tester.widget<Checkbox>(checkbox).value, true);
});
testWidgets('CheckboxListTile respects mouseCursor when hovered', (WidgetTester tester) async {
// Test Checkbox() constructor
await tester.pumpWidget(
wrap(
child: MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: CheckboxListTile(
mouseCursor: SystemMouseCursors.text,
value: true,
onChanged: (_) {},
),
),
),
);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
await gesture.addPointer(location: tester.getCenter(find.byType(Checkbox)));
await tester.pump();
await gesture.moveTo(tester.getCenter(find.byType(Checkbox)));
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
// Test default cursor
await tester.pumpWidget(
wrap(
child: CheckboxListTile(
value: true,
onChanged: (_) {},
),
),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click);
// Test default cursor when disabled
await tester.pumpWidget(
wrap(
child: const MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: CheckboxListTile(
value: true,
onChanged: null,
),
),
),
);
await gesture.moveTo(tester.getCenter(find.byType(Checkbox)));
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
// Test cursor when tristate
await tester.pumpWidget(
wrap(
child: const MouseRegion(
cursor: SystemMouseCursors.forbidden,
child: CheckboxListTile(
value: null,
tristate: true,
onChanged: null,
mouseCursor: _SelectedGrabMouseCursor(),
),
),
),
);
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.grab);
await tester.pumpAndSettle();
});
testWidgets('CheckboxListTile respects fillColor in enabled/disabled states', (WidgetTester tester) async {
const Color activeEnabledFillColor = Color(0xFF000001);
const Color activeDisabledFillColor = Color(0xFF000002);
Color getFillColor(Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return activeDisabledFillColor;
}
return activeEnabledFillColor;
}
final MaterialStateProperty<Color> fillColor = MaterialStateColor.resolveWith(getFillColor);
Widget buildFrame({required bool enabled}) {
return wrap(
child: CheckboxListTile(
value: true,
fillColor: fillColor,
onChanged: enabled ? (bool? value) { } : null,
)
);
}
RenderBox getCheckboxRenderer() {
return tester.renderObject<RenderBox>(find.byType(Checkbox));
}
await tester.pumpWidget(buildFrame(enabled: true));
await tester.pumpAndSettle();
expect(getCheckboxRenderer(), paints..path(color: activeEnabledFillColor));
await tester.pumpWidget(buildFrame(enabled: false));
await tester.pumpAndSettle();
expect(getCheckboxRenderer(), paints..path(color: activeDisabledFillColor));
});
testWidgets('CheckboxListTile respects fillColor in hovered state', (WidgetTester tester) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
const Color hoveredFillColor = Color(0xFF000001);
Color getFillColor(Set<MaterialState> states) {
if (states.contains(MaterialState.hovered)) {
return hoveredFillColor;
}
return Colors.transparent;
}
final MaterialStateProperty<Color> fillColor =
MaterialStateColor.resolveWith(getFillColor);
Widget buildFrame() {
return wrap(
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return CheckboxListTile(
value: true,
fillColor: fillColor,
onChanged: (bool? value) { },
);
},
),
);
}
RenderBox getCheckboxRenderer() {
return tester.renderObject<RenderBox>(find.byType(Checkbox));
}
await tester.pumpWidget(buildFrame());
await tester.pumpAndSettle();
// Start hovering
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(find.byType(Checkbox)));
await tester.pumpAndSettle();
expect(getCheckboxRenderer(), paints..path(color: hoveredFillColor));
});
testWidgets('CheckboxListTile respects hoverColor', (WidgetTester tester) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
bool? value = true;
Widget buildApp({bool enabled = true}) {
return wrap(
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return CheckboxListTile(
value: value,
onChanged: enabled ? (bool? newValue) {
setState(() {
value = newValue;
});
} : null,
hoverColor: Colors.orange[500],
);
}),
);
}
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints
..path(style: PaintingStyle.fill)
..path(style: PaintingStyle.stroke, strokeWidth: 2.0),
);
// Start hovering
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.moveTo(tester.getCenter(find.byType(Checkbox)));
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints
..circle(color: Colors.orange[500])
..path(style: PaintingStyle.fill)
..path(style: PaintingStyle.stroke, strokeWidth: 2.0),
);
// Check what happens when disabled.
await tester.pumpWidget(buildApp(enabled: false));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints
..path(style: PaintingStyle.fill)
..path(style: PaintingStyle.stroke, strokeWidth: 2.0),
);
});
testWidgets('CheckboxListTile respects overlayColor in active/pressed/hovered states', (WidgetTester tester) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
const Color fillColor = Color(0xFF000000);
const Color activePressedOverlayColor = Color(0xFF000001);
const Color inactivePressedOverlayColor = Color(0xFF000002);
const Color hoverOverlayColor = Color(0xFF000003);
const Color hoverColor = Color(0xFF000005);
Color? getOverlayColor(Set<MaterialState> states) {
if (states.contains(MaterialState.pressed)) {
if (states.contains(MaterialState.selected)) {
return activePressedOverlayColor;
}
return inactivePressedOverlayColor;
}
if (states.contains(MaterialState.hovered)) {
return hoverOverlayColor;
}
return null;
}
const double splashRadius = 24.0;
Widget buildCheckbox({bool active = false, bool useOverlay = true}) {
return wrap(
child: CheckboxListTile(
value: active,
onChanged: (_) { },
fillColor: const MaterialStatePropertyAll<Color>(fillColor),
overlayColor: useOverlay ? MaterialStateProperty.resolveWith(getOverlayColor) : null,
hoverColor: hoverColor,
splashRadius: splashRadius,
),
);
}
await tester.pumpWidget(buildCheckbox(useOverlay: false));
await tester.startGesture(tester.getCenter(find.byType(Checkbox)));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints..circle()
..circle(
color: fillColor.withAlpha(kRadialReactionAlpha),
radius: splashRadius,
),
reason: 'Default inactive pressed Checkbox should have overlay color from fillColor',
);
await tester.pumpWidget(buildCheckbox(active: true, useOverlay: false));
await tester.startGesture(tester.getCenter(find.byType(Checkbox)));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints..circle()
..circle(
color: fillColor.withAlpha(kRadialReactionAlpha),
radius: splashRadius,
),
reason: 'Default active pressed Checkbox should have overlay color from fillColor',
);
await tester.pumpWidget(buildCheckbox());
await tester.startGesture(tester.getCenter(find.byType(Checkbox)));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints..circle()
..circle(
color: inactivePressedOverlayColor,
radius: splashRadius,
),
reason: 'Inactive pressed Checkbox should have overlay color: $inactivePressedOverlayColor',
);
await tester.pumpWidget(buildCheckbox(active: true));
await tester.startGesture(tester.getCenter(find.byType(Checkbox)));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints..circle()
..circle(
color: activePressedOverlayColor,
radius: splashRadius,
),
reason: 'Active pressed Checkbox should have overlay color: $activePressedOverlayColor',
);
// Start hovering
await tester.pumpWidget(Container());
await tester.pumpWidget(buildCheckbox());
await tester.pumpAndSettle();
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(find.byType(Checkbox)));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints
..circle(
color: hoverOverlayColor,
radius: splashRadius,
),
reason: 'Hovered Checkbox should use overlay color $hoverOverlayColor over $hoverColor',
);
});
testWidgets('CheckboxListTile respects splashRadius', (WidgetTester tester) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
const double splashRadius = 30;
Widget buildApp() {
return wrap(
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return CheckboxListTile(
value: false,
onChanged: (bool? newValue) {},
hoverColor: Colors.orange[500],
splashRadius: splashRadius,
);
}),
);
}
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(find.byType(Checkbox)));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints..circle(color: Colors.orange[500], radius: splashRadius),
);
});
testWidgets('CheckboxListTile respects materialTapTargetSize', (WidgetTester tester) async {
await tester.pumpWidget(
wrap(
child: CheckboxListTile(
value: true,
onChanged: (bool? newValue) { },
),
),
);
// default test
expect(tester.getSize(find.byType(Checkbox)), const Size(40.0, 40.0));
await tester.pumpWidget(
wrap(
child: CheckboxListTile(
materialTapTargetSize: MaterialTapTargetSize.padded,
value: true,
onChanged: (bool? newValue) { },
),
),
);
expect(tester.getSize(find.byType(Checkbox)), const Size(48.0, 48.0));
});
testWidgets('CheckboxListTile respects isError - M3', (WidgetTester tester) async {
final ThemeData themeData = ThemeData(useMaterial3: true);
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
bool? value = true;
Widget buildApp() {
return MaterialApp(
theme: themeData,
home: Material(
child: Center(
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
return CheckboxListTile(
isError: true,
value: value,
onChanged: (bool? newValue) {
setState(() {
value = newValue;
});
},
);
}),
),
),
);
}
// Default color
await tester.pumpWidget(Container());
await tester.pumpWidget(buildApp());
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints..path(color: themeData.colorScheme.error)..path(color: themeData.colorScheme.onError)
);
// Start hovering
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.moveTo(tester.getCenter(find.byType(Checkbox)));
await tester.pumpAndSettle();
expect(
Material.of(tester.element(find.byType(Checkbox))),
paints
..circle(color: themeData.colorScheme.error.withOpacity(0.08))
..path(color: themeData.colorScheme.error)
);
});
group('feedback', () {
late FeedbackTester feedback;
......@@ -541,3 +961,18 @@ void main() {
});
});
}
class _SelectedGrabMouseCursor extends MaterialStateMouseCursor {
const _SelectedGrabMouseCursor();
@override
MouseCursor resolve(Set<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
return SystemMouseCursors.grab;
}
return SystemMouseCursors.basic;
}
@override
String get debugDescription => '_SelectedGrabMouseCursor()';
}
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