Make `CupertinoRadio`'s `mouseCursor` a `WidgetStateProperty` (#151910)
https://github.com/flutter/flutter/pull/149681 introduced `mouseCursor `to `CupertinoRadio` as a `MouseCursor` instead of a `WidgetStateProperty` to match Material Radio's `mouseCursor` property for `.adaptive`.
This PR changes `mouseCursor` to be of type `WidgetStateProperty<MouseCursor>` as per review comments in https://github.com/flutter/flutter/pull/151788#discussion_r1680538286.
PR bringing `mouseCursor` into `CupertinoRadio`: https://github.com/flutter/flutter/pull/149681.
Part of https://github.com/flutter/flutter/issues/58192
diff --git a/packages/flutter/lib/src/cupertino/radio.dart b/packages/flutter/lib/src/cupertino/radio.dart
index 9ef3b83..39a5dd3 100644
--- a/packages/flutter/lib/src/cupertino/radio.dart
+++ b/packages/flutter/lib/src/cupertino/radio.dart
@@ -132,24 +132,20 @@
/// The cursor for a mouse pointer when it enters or is hovering over the
/// widget.
///
- /// If [mouseCursor] is a [WidgetStateMouseCursor],
- /// [WidgetStateMouseCursor.resolve] is used for the following [WidgetState]s:
+ /// Resolves in the following states:
///
/// * [WidgetState.selected].
- /// * [WidgetState.hovered].
/// * [WidgetState.focused].
/// * [WidgetState.disabled].
///
- /// If null, then [SystemMouseCursors.basic] is used when this radio button is disabled.
- /// When this radio button is enabled, [SystemMouseCursors.click] is used on Web, and
- /// [SystemMouseCursors.basic] is used on other platforms.
+ /// Defaults to [defaultMouseCursor].
///
/// See also:
///
/// * [WidgetStateMouseCursor], a [MouseCursor] that implements
/// `WidgetStateProperty` which is used in APIs that need to accept
- /// either a [MouseCursor] or a [WidgetStateProperty<MouseCursor>].
- final MouseCursor? mouseCursor;
+ /// either a [MouseCursor] or a [WidgetStateProperty].
+ final WidgetStateProperty<MouseCursor>? mouseCursor;
/// Set to true if this radio button is allowed to be returned to an
/// indeterminate state by selecting it again when selected.
@@ -210,6 +206,18 @@
bool get _selected => value == groupValue;
+ /// The default [mouseCursor] of a [CupertinoRadio].
+ ///
+ /// If [onChanged] is null, indicating the radio button is disabled,
+ /// [SystemMouseCursors.basic] is used. Otherwise, [SystemMouseCursors.click]
+ /// is used on Web, and [SystemMouseCursors.basic] is used on other platforms.
+ static WidgetStateProperty<MouseCursor> defaultMouseCursor(Function? onChanged) {
+ final MouseCursor mouseCursor = (onChanged != null && kIsWeb)
+ ? SystemMouseCursors.click
+ : SystemMouseCursors.basic;
+ return WidgetStateProperty.all<MouseCursor>(mouseCursor);
+ }
+
@override
State<CupertinoRadio<T>> createState() => _CupertinoRadioState<T>();
}
@@ -269,15 +277,6 @@
final Color effectiveFillColor = widget.fillColor ?? CupertinoColors.white;
- final WidgetStateProperty<MouseCursor> effectiveMouseCursor =
- WidgetStateProperty.resolveWith<MouseCursor>((Set<WidgetState> states) {
- return WidgetStateProperty.resolveAs<MouseCursor?>(widget.mouseCursor, states)
- ?? (states.contains(WidgetState.disabled)
- ? SystemMouseCursors.basic
- : kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic
- );
- });
-
final bool? accessibilitySelected;
// Apple devices also use `selected` to annotate radio button's semantics
// state.
@@ -297,7 +296,7 @@
checked: widget._selected,
selected: accessibilitySelected,
child: buildToggleable(
- mouseCursor: effectiveMouseCursor,
+ mouseCursor: widget.mouseCursor ?? CupertinoRadio.defaultMouseCursor(widget.onChanged),
focusNode: widget.focusNode,
autofocus: widget.autofocus,
onFocusChange: onFocusChange,
diff --git a/packages/flutter/lib/src/material/radio.dart b/packages/flutter/lib/src/material/radio.dart
index a574e9c..2dad655 100644
--- a/packages/flutter/lib/src/material/radio.dart
+++ b/packages/flutter/lib/src/material/radio.dart
@@ -447,7 +447,11 @@
value: widget.value,
groupValue: widget.groupValue,
onChanged: widget.onChanged,
- mouseCursor: widget.mouseCursor,
+ mouseCursor: widget.mouseCursor == null
+ ? CupertinoRadio.defaultMouseCursor(widget.onChanged)
+ : WidgetStateProperty.resolveWith((Set<MaterialState> states) {
+ return WidgetStateProperty.resolveAs<MouseCursor>(widget.mouseCursor!, states);
+ }),
toggleable: widget.toggleable,
activeColor: widget.activeColor,
focusColor: widget.focusColor,
diff --git a/packages/flutter/test/cupertino/radio_test.dart b/packages/flutter/test/cupertino/radio_test.dart
index f44369a..483fcf1 100644
--- a/packages/flutter/test/cupertino/radio_test.dart
+++ b/packages/flutter/test/cupertino/radio_test.dart
@@ -441,7 +441,7 @@
value: 1,
groupValue: 1,
onChanged: (int? i) { },
- mouseCursor: SystemMouseCursors.forbidden,
+ mouseCursor: WidgetStateProperty.all(SystemMouseCursors.forbidden),
),
),
));
@@ -463,13 +463,25 @@
final FocusNode focusNode = FocusNode(debugLabel: 'Radio');
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
+ MouseCursor getMouseCursor(Set<WidgetState> states) {
+ if (states.contains(WidgetState.disabled)) {
+ return SystemMouseCursors.forbidden;
+ }
+ if (states.contains(WidgetState.focused)) {
+ return SystemMouseCursors.basic;
+ }
+ return SystemMouseCursors.click;
+ }
+
+ final WidgetStateProperty<MouseCursor> mouseCursor = WidgetStateProperty.resolveWith(getMouseCursor);
+
await tester.pumpWidget(CupertinoApp(
home: Center(
child: CupertinoRadio<int>(
value: 1,
groupValue: 1,
onChanged: (int? i) { },
- mouseCursor: const RadioMouseCursor(),
+ mouseCursor: mouseCursor,
focusNode: focusNode
),
),
@@ -498,13 +510,13 @@
);
// Test disabled case.
- await tester.pumpWidget(const CupertinoApp(
+ await tester.pumpWidget(CupertinoApp(
home: Center(
child: CupertinoRadio<int>(
value: 1,
groupValue: 1,
onChanged: null,
- mouseCursor: RadioMouseCursor(),
+ mouseCursor: mouseCursor,
),
),
));
@@ -541,21 +553,3 @@
);
});
}
-
-class RadioMouseCursor extends WidgetStateMouseCursor {
- const RadioMouseCursor();
-
- @override
- MouseCursor resolve(Set<WidgetState> states) {
- if (states.contains(WidgetState.disabled)) {
- return SystemMouseCursors.forbidden;
- }
- if (states.contains(WidgetState.focused)){
- return SystemMouseCursors.basic;
- }
- return SystemMouseCursors.click;
- }
-
- @override
- String get debugDescription => 'RadioMouseCursor()';
-}
diff --git a/packages/flutter/test/material/radio_test.dart b/packages/flutter/test/material/radio_test.dart
index c3ede77..e9f4225 100644
--- a/packages/flutter/test/material/radio_test.dart
+++ b/packages/flutter/test/material/radio_test.dart
@@ -1859,6 +1859,46 @@
}
});
+ testWidgets('Radio.adaptive respects Radio.mouseCursor', (WidgetTester tester) async {
+ Widget buildApp({required TargetPlatform platform, MouseCursor? mouseCursor}) {
+ return MaterialApp(
+ theme: ThemeData(platform: platform),
+ home: Material(
+ child: Radio<int>.adaptive(
+ value: 1,
+ groupValue: 1,
+ onChanged: (int? i) {},
+ mouseCursor: mouseCursor,
+ ),
+ ),
+ );
+ }
+
+ for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.iOS, TargetPlatform.macOS ]) {
+ await tester.pumpWidget(buildApp(platform: platform));
+ final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
+
+ // Test default mouse cursor.
+ await gesture.addPointer(location: tester.getCenter(find.byType(CupertinoRadio<int>)));
+ await tester.pump();
+ await gesture.moveTo(tester.getCenter(find.byType(CupertinoRadio<int>)));
+ expect(
+ RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
+ kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic,
+ );
+
+ // Test mouse cursor can be configured.
+ await tester.pumpWidget(buildApp(platform: platform, mouseCursor: SystemMouseCursors.forbidden));
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.forbidden);
+
+ // Test Radio.adaptive can resolve a WidgetStateMouseCursor.
+ await tester.pumpWidget(buildApp(platform: platform, mouseCursor: const _SelectedGrabMouseCursor()));
+ expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.grab);
+
+ await gesture.removePointer();
+ }
+ });
+
testWidgets('Material2 - Radio default overlayColor and fillColor resolves pressed state', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'Radio');
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
@@ -1993,3 +2033,18 @@
focusNode.dispose();
});
}
+
+class _SelectedGrabMouseCursor extends WidgetStateMouseCursor {
+ const _SelectedGrabMouseCursor();
+
+ @override
+ MouseCursor resolve(Set<WidgetState> states) {
+ if (states.contains(WidgetState.selected)) {
+ return SystemMouseCursors.grab;
+ }
+ return SystemMouseCursors.basic;
+ }
+
+ @override
+ String get debugDescription => '_SelectedGrabMouseCursor()';
+}