From 628ab7f4932b00659cfbd6ebb30f35e451d0ccfd Mon Sep 17 00:00:00 2001 From: Valentin Vignal <32538273+ValentinVignal@users.noreply.github.com> Date: Wed, 4 Dec 2024 12:56:01 +0800 Subject: [PATCH] Add `mouseCursor` parameter to `Chip`s (#159422) Part of https://github.com/flutter/flutter/issues/58192 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --------- Co-authored-by: Victor Sanni --- .../flutter/lib/src/material/action_chip.dart | 5 ++ packages/flutter/lib/src/material/chip.dart | 21 +++++ .../flutter/lib/src/material/choice_chip.dart | 5 ++ .../flutter/lib/src/material/filter_chip.dart | 5 ++ .../flutter/lib/src/material/input_chip.dart | 4 + .../flutter/test/cupertino/checkbox_test.dart | 2 +- .../test/material/action_chip_test.dart | 77 ++++++++++++++++++ packages/flutter/test/material/chip_test.dart | 56 +++++++++++++ .../test/material/choice_chip_test.dart | 79 +++++++++++++++++++ .../test/material/filter_chip_test.dart | 76 ++++++++++++++++++ .../test/material/input_chip_test.dart | 75 ++++++++++++++++++ 11 files changed, 404 insertions(+), 1 deletion(-) diff --git a/packages/flutter/lib/src/material/action_chip.dart b/packages/flutter/lib/src/material/action_chip.dart index 5d58e0a3eb..f13f25dce6 100644 --- a/packages/flutter/lib/src/material/action_chip.dart +++ b/packages/flutter/lib/src/material/action_chip.dart @@ -120,6 +120,7 @@ class ActionChip extends StatelessWidget implements ChipAttributes, TappableChip this.iconTheme, this.avatarBoxConstraints, this.chipAnimationStyle, + this.mouseCursor, }) : assert(pressElevation == null || pressElevation >= 0.0), assert(elevation == null || elevation >= 0.0), _chipVariant = _ChipVariant.flat; @@ -156,6 +157,7 @@ class ActionChip extends StatelessWidget implements ChipAttributes, TappableChip this.iconTheme, this.avatarBoxConstraints, this.chipAnimationStyle, + this.mouseCursor, }) : assert(pressElevation == null || pressElevation >= 0.0), assert(elevation == null || elevation >= 0.0), _chipVariant = _ChipVariant.elevated; @@ -208,6 +210,8 @@ class ActionChip extends StatelessWidget implements ChipAttributes, TappableChip final BoxConstraints? avatarBoxConstraints; @override final ChipAnimationStyle? chipAnimationStyle; + @override + final MouseCursor? mouseCursor; @override bool get isEnabled => onPressed != null; @@ -247,6 +251,7 @@ class ActionChip extends StatelessWidget implements ChipAttributes, TappableChip iconTheme: iconTheme, avatarBoxConstraints: avatarBoxConstraints, chipAnimationStyle: chipAnimationStyle, + mouseCursor: mouseCursor, ); } } diff --git a/packages/flutter/lib/src/material/chip.dart b/packages/flutter/lib/src/material/chip.dart index 628b223667..9f9bf4b63b 100644 --- a/packages/flutter/lib/src/material/chip.dart +++ b/packages/flutter/lib/src/material/chip.dart @@ -278,6 +278,19 @@ abstract interface class ChipAttributes { /// ** See code in examples/api/lib/material/chip/chip_attributes.chip_animation_style.0.dart ** /// {@end-tool} ChipAnimationStyle? get chipAnimationStyle; + + /// The cursor for a mouse pointer when it enters or is hovering over the + /// widget. + /// + /// If [mouseCursor] is a [WidgetStateMouseCursor], + /// [WidgetStateProperty.resolve] is used for the following [WidgetState]s: + /// + /// * [WidgetState.hovered]. + /// * [WidgetState.focused]. + /// * [WidgetState.disabled]. + /// + /// If this property is null, [WidgetStateMouseCursor.clickable] will be used. + MouseCursor? get mouseCursor; } /// An interface for Material Design chips that can be deleted. @@ -704,6 +717,7 @@ class Chip extends StatelessWidget implements ChipAttributes, DeletableChipAttri this.avatarBoxConstraints, this.deleteIconBoxConstraints, this.chipAnimationStyle, + this.mouseCursor, }) : assert(elevation == null || elevation >= 0.0); @override @@ -756,6 +770,8 @@ class Chip extends StatelessWidget implements ChipAttributes, DeletableChipAttri final BoxConstraints? deleteIconBoxConstraints; @override final ChipAnimationStyle? chipAnimationStyle; + @override + final MouseCursor? mouseCursor; @override Widget build(BuildContext context) { @@ -787,6 +803,7 @@ class Chip extends StatelessWidget implements ChipAttributes, DeletableChipAttri avatarBoxConstraints: avatarBoxConstraints, deleteIconBoxConstraints: deleteIconBoxConstraints, chipAnimationStyle: chipAnimationStyle, + mouseCursor: mouseCursor, ); } } @@ -877,6 +894,7 @@ class RawChip extends StatefulWidget this.avatarBoxConstraints, this.deleteIconBoxConstraints, this.chipAnimationStyle, + this.mouseCursor, }) : assert(pressElevation == null || pressElevation >= 0.0), assert(elevation == null || elevation >= 0.0), deleteIcon = deleteIcon ?? _kDefaultDeleteIcon; @@ -962,6 +980,8 @@ class RawChip extends StatefulWidget final BoxConstraints? deleteIconBoxConstraints; @override final ChipAnimationStyle? chipAnimationStyle; + @override + final MouseCursor? mouseCursor; /// If set, this indicates that the chip should be disabled if all of the /// tap callbacks ([onSelected], [onPressed]) are null. @@ -1407,6 +1427,7 @@ class _RawChipState extends State with MaterialStateMixin, TickerProvid onTapDown: canTap ? _handleTapDown : null, onTapCancel: canTap ? _handleTapCancel : null, onHover: canTap ? updateMaterialState(MaterialState.hovered) : null, + mouseCursor: widget.mouseCursor, hoverColor: (widget.color ?? chipTheme.color) == null ? null : Colors.transparent, customBorder: resolvedShape, child: AnimatedBuilder( diff --git a/packages/flutter/lib/src/material/choice_chip.dart b/packages/flutter/lib/src/material/choice_chip.dart index c6729d6717..d756d37297 100644 --- a/packages/flutter/lib/src/material/choice_chip.dart +++ b/packages/flutter/lib/src/material/choice_chip.dart @@ -101,6 +101,7 @@ class ChoiceChip extends StatelessWidget this.avatarBorder = const CircleBorder(), this.avatarBoxConstraints, this.chipAnimationStyle, + this.mouseCursor, }) : assert(pressElevation == null || pressElevation >= 0.0), assert(elevation == null || elevation >= 0.0), _chipVariant = _ChipVariant.flat; @@ -143,6 +144,7 @@ class ChoiceChip extends StatelessWidget this.avatarBorder = const CircleBorder(), this.avatarBoxConstraints, this.chipAnimationStyle, + this.mouseCursor, }) : assert(pressElevation == null || pressElevation >= 0.0), assert(elevation == null || elevation >= 0.0), _chipVariant = _ChipVariant.elevated; @@ -207,6 +209,8 @@ class ChoiceChip extends StatelessWidget final BoxConstraints? avatarBoxConstraints; @override final ChipAnimationStyle? chipAnimationStyle; + @override + final MouseCursor? mouseCursor; @override bool get isEnabled => onSelected != null; @@ -253,6 +257,7 @@ class ChoiceChip extends StatelessWidget iconTheme: iconTheme, avatarBoxConstraints: avatarBoxConstraints, chipAnimationStyle: chipAnimationStyle, + mouseCursor: mouseCursor, ); } } diff --git a/packages/flutter/lib/src/material/filter_chip.dart b/packages/flutter/lib/src/material/filter_chip.dart index 7fa508a64f..e970c23a8f 100644 --- a/packages/flutter/lib/src/material/filter_chip.dart +++ b/packages/flutter/lib/src/material/filter_chip.dart @@ -113,6 +113,7 @@ class FilterChip extends StatelessWidget this.avatarBoxConstraints, this.deleteIconBoxConstraints, this.chipAnimationStyle, + this.mouseCursor, }) : assert(pressElevation == null || pressElevation >= 0.0), assert(elevation == null || elevation >= 0.0), _chipVariant = _ChipVariant.flat; @@ -160,6 +161,7 @@ class FilterChip extends StatelessWidget this.avatarBoxConstraints, this.deleteIconBoxConstraints, this.chipAnimationStyle, + this.mouseCursor, }) : assert(pressElevation == null || pressElevation >= 0.0), assert(elevation == null || elevation >= 0.0), _chipVariant = _ChipVariant.elevated; @@ -234,6 +236,8 @@ class FilterChip extends StatelessWidget final BoxConstraints? deleteIconBoxConstraints; @override final ChipAnimationStyle? chipAnimationStyle; + @override + final MouseCursor? mouseCursor; @override bool get isEnabled => onSelected != null; @@ -286,6 +290,7 @@ class FilterChip extends StatelessWidget avatarBoxConstraints: avatarBoxConstraints, deleteIconBoxConstraints: deleteIconBoxConstraints, chipAnimationStyle: chipAnimationStyle, + mouseCursor: mouseCursor, ); } } diff --git a/packages/flutter/lib/src/material/input_chip.dart b/packages/flutter/lib/src/material/input_chip.dart index 28a819b6aa..a2ac93ed24 100644 --- a/packages/flutter/lib/src/material/input_chip.dart +++ b/packages/flutter/lib/src/material/input_chip.dart @@ -132,6 +132,7 @@ class InputChip extends StatelessWidget this.avatarBoxConstraints, this.deleteIconBoxConstraints, this.chipAnimationStyle, + this.mouseCursor, }) : assert(pressElevation == null || pressElevation >= 0.0), assert(elevation == null || elevation >= 0.0); @@ -209,6 +210,8 @@ class InputChip extends StatelessWidget final BoxConstraints? deleteIconBoxConstraints; @override final ChipAnimationStyle? chipAnimationStyle; + @override + final MouseCursor? mouseCursor; @override Widget build(BuildContext context) { @@ -257,6 +260,7 @@ class InputChip extends StatelessWidget avatarBoxConstraints: avatarBoxConstraints, deleteIconBoxConstraints: deleteIconBoxConstraints, chipAnimationStyle: chipAnimationStyle, + mouseCursor: mouseCursor, ); } } diff --git a/packages/flutter/test/cupertino/checkbox_test.dart b/packages/flutter/test/cupertino/checkbox_test.dart index 8363bce28f..d105a473a2 100644 --- a/packages/flutter/test/cupertino/checkbox_test.dart +++ b/packages/flutter/test/cupertino/checkbox_test.dart @@ -516,7 +516,7 @@ void main() { home: Center( child: CupertinoCheckbox( value: value, - onChanged: enabled ? (bool? value) => true : null, + onChanged: enabled ? (bool? value) {} : null, mouseCursor: const _CheckboxMouseCursor(), focusNode: focusNode ), diff --git a/packages/flutter/test/material/action_chip_test.dart b/packages/flutter/test/material/action_chip_test.dart index dc974a3670..1ba6b1895d 100644 --- a/packages/flutter/test/material/action_chip_test.dart +++ b/packages/flutter/test/material/action_chip_test.dart @@ -2,7 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:ui'; + import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; /// Adds the basic requirements for a Chip. @@ -529,4 +533,77 @@ void main() { expect(tester.widget(find.byType(RawChip)).chipAnimationStyle, chipAnimationStyle); }); + + testWidgets('ActionChip mouse cursor behavior', (WidgetTester tester) async { + const SystemMouseCursor customCursor = SystemMouseCursors.grab; + + await tester.pumpWidget(wrapForChip( + child: const Center( + child: ActionChip( + mouseCursor: customCursor, + label: Text('Chip'), + ), + ), + )); + + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1); + await gesture.addPointer(location: const Offset(10, 10)); + await tester.pump(); + expect( + RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), + SystemMouseCursors.basic, + ); + + final Offset chip = tester.getCenter(find.text('Chip')); + await gesture.moveTo(chip); + await tester.pump(); + expect( + RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), + customCursor, + ); + }); + + testWidgets('Mouse cursor resolves in focused/unfocused/disabled states', (WidgetTester tester) async { + tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; + final FocusNode focusNode = FocusNode(debugLabel: 'Chip'); + addTearDown(focusNode.dispose); + + Widget buildChip({ required bool enabled }) { + return wrapForChip( + child: Center( + child: ActionChip( + mouseCursor: const WidgetStateMouseCursor.fromMap( + { + WidgetState.disabled: SystemMouseCursors.forbidden, + WidgetState.focused: SystemMouseCursors.grab, + WidgetState.any: SystemMouseCursors.basic, + }, + ), + focusNode: focusNode, + label: const Text('Chip'), + onPressed: enabled ? () {} : null, + ), + ), + ); + } + + await tester.pumpWidget(buildChip(enabled: true)); + + // Unfocused case. + final TestGesture gesture1 = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1); + addTearDown(gesture1.removePointer); + await gesture1.addPointer(location: tester.getCenter(find.text('Chip'))); + await tester.pump(); + await gesture1.moveTo(tester.getCenter(find.text('Chip'))); + expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic); + + // Focused case. + focusNode.requestFocus(); + await tester.pump(); + expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.grab); + + // Disabled case. + await tester.pumpWidget(buildChip(enabled: false)); + expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.forbidden); + }); } diff --git a/packages/flutter/test/material/chip_test.dart b/packages/flutter/test/material/chip_test.dart index 25d0b11a17..ea8fbde35b 100644 --- a/packages/flutter/test/material/chip_test.dart +++ b/packages/flutter/test/material/chip_test.dart @@ -12,6 +12,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import '../widgets/feedback_tester.dart'; import '../widgets/semantics_tester.dart'; @@ -6082,6 +6083,61 @@ void main() { isNot(paints..rrect(color: hoverColor)..rect(color: themeDataHoverColor)), ); }); + + testWidgets('Chip mouse cursor behavior', (WidgetTester tester) async { + const SystemMouseCursor customCursor = SystemMouseCursors.grab; + + await tester.pumpWidget(wrapForChip( + child: const Center( + child: Chip( + mouseCursor: customCursor, + label: Text('Chip'), + ), + ), + )); + + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1); + await gesture.addPointer(location: const Offset(10, 10)); + await tester.pump(); + expect( + RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), + SystemMouseCursors.basic, + ); + + final Offset chip = tester.getCenter(find.text('Chip')); + await gesture.moveTo(chip); + await tester.pump(); + expect( + RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), + customCursor, + ); + }); + + testWidgets('Mouse cursor resolves in disabled states', (WidgetTester tester) async { + tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; + + await tester.pumpWidget( + wrapForChip( + child: const Center( + child: Chip( + mouseCursor: WidgetStateMouseCursor.fromMap( + { + WidgetState.disabled: SystemMouseCursors.forbidden, + }, + ), + label: Text('Chip'), + ), + ), + ), + ); + // Unfocused case. + final TestGesture gesture1 = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1); + addTearDown(gesture1.removePointer); + await gesture1.addPointer(location: tester.getCenter(find.text('Chip'))); + await tester.pump(); + await gesture1.moveTo(tester.getCenter(find.text('Chip'))); + expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.forbidden); + }); } class _MaterialStateOutlinedBorder extends StadiumBorder implements MaterialStateOutlinedBorder { diff --git a/packages/flutter/test/material/choice_chip_test.dart b/packages/flutter/test/material/choice_chip_test.dart index 76ae42cfd1..8bb26081b7 100644 --- a/packages/flutter/test/material/choice_chip_test.dart +++ b/packages/flutter/test/material/choice_chip_test.dart @@ -2,7 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:ui'; + import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; RenderBox getMaterialBox(WidgetTester tester, Finder type) { @@ -813,4 +817,79 @@ void main() { expect(tester.widget(find.byType(RawChip)).chipAnimationStyle, chipAnimationStyle); }); + + testWidgets('ChoiceChip mouse cursor behavior', (WidgetTester tester) async { + const SystemMouseCursor customCursor = SystemMouseCursors.grab; + + await tester.pumpWidget(wrapForChip( + child: const Center( + child: ChoiceChip( + selected: false, + mouseCursor: customCursor, + label: Text('Chip'), + ), + ), + )); + + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1); + await gesture.addPointer(location: const Offset(10, 10)); + await tester.pump(); + expect( + RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), + SystemMouseCursors.basic, + ); + + final Offset chip = tester.getCenter(find.text('Chip')); + await gesture.moveTo(chip); + await tester.pump(); + expect( + RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), + customCursor, + ); + }); + + testWidgets('Mouse cursor resolves in focused/unfocused/disabled states', (WidgetTester tester) async { + tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; + final FocusNode focusNode = FocusNode(debugLabel: 'Chip'); + addTearDown(focusNode.dispose); + + Widget buildChip({ required bool enabled }) { + return wrapForChip( + child: Center( + child: ChoiceChip( + mouseCursor: const WidgetStateMouseCursor.fromMap( + { + WidgetState.disabled: SystemMouseCursors.forbidden, + WidgetState.focused: SystemMouseCursors.grab, + WidgetState.selected: SystemMouseCursors.click, + WidgetState.any: SystemMouseCursors.basic, + }, + ), + focusNode: focusNode, + label: const Text('Chip'), + onSelected: enabled ? (bool value) {} : null, + selected: false, + ), + ), + ); + } + + // Unfocused case. + await tester.pumpWidget(buildChip(enabled: true)); + final TestGesture gesture1 = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1); + addTearDown(gesture1.removePointer); + await gesture1.addPointer(location: tester.getCenter(find.text('Chip'))); + await tester.pump(); + await gesture1.moveTo(tester.getCenter(find.text('Chip'))); + expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic); + + // Focused case. + focusNode.requestFocus(); + await tester.pump(); + expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.grab); + + // Disabled case. + await tester.pumpWidget(buildChip(enabled: false)); + expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.forbidden); + }); } diff --git a/packages/flutter/test/material/filter_chip_test.dart b/packages/flutter/test/material/filter_chip_test.dart index 926e4683b6..2354ec077f 100644 --- a/packages/flutter/test/material/filter_chip_test.dart +++ b/packages/flutter/test/material/filter_chip_test.dart @@ -10,6 +10,8 @@ library; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import '../widgets/feedback_tester.dart'; @@ -1324,4 +1326,78 @@ void main() { expect(tester.widget(find.byType(RawChip)).chipAnimationStyle, chipAnimationStyle); }); + + testWidgets('FilterChip mouse cursor behavior', (WidgetTester tester) async { + const SystemMouseCursor customCursor = SystemMouseCursors.grab; + + await tester.pumpWidget(wrapForChip( + child: Center( + child: FilterChip( + mouseCursor: customCursor, + label: const Text('Chip'), + onSelected: (bool value) {}, + ), + ), + )); + + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1); + await gesture.addPointer(location: const Offset(10, 10)); + await tester.pump(); + expect( + RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), + SystemMouseCursors.basic, + ); + + final Offset chip = tester.getCenter(find.text('Chip')); + await gesture.moveTo(chip); + await tester.pump(); + expect( + RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), + customCursor, + ); + }); + + testWidgets('Mouse cursor resolves in focused/unfocused/disabled states', (WidgetTester tester) async { + tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; + final FocusNode focusNode = FocusNode(debugLabel: 'Chip'); + addTearDown(focusNode.dispose); + + Widget buildChip({ required bool enabled }) { + return wrapForChip( + child: Center( + child: FilterChip( + mouseCursor: const WidgetStateMouseCursor.fromMap( + { + WidgetState.disabled: SystemMouseCursors.forbidden, + WidgetState.focused: SystemMouseCursors.grab, + WidgetState.selected: SystemMouseCursors.click, + WidgetState.any: SystemMouseCursors.basic, + }, + ), + focusNode: focusNode, + label: const Text('Chip'), + onSelected: enabled ? (bool value) {} : null, + ), + ), + ); + } + + // Unfocused case. + await tester.pumpWidget(buildChip(enabled: true)); + final TestGesture gesture1 = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1); + addTearDown(gesture1.removePointer); + await gesture1.addPointer(location: tester.getCenter(find.text('Chip'))); + await tester.pump(); + await gesture1.moveTo(tester.getCenter(find.text('Chip'))); + expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic); + + // Focused case. + focusNode.requestFocus(); + await tester.pump(); + expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.grab); + + // Disabled case. + await tester.pumpWidget(buildChip(enabled: false)); + expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.forbidden); + }); } diff --git a/packages/flutter/test/material/input_chip_test.dart b/packages/flutter/test/material/input_chip_test.dart index ea00c019f9..34c2111724 100644 --- a/packages/flutter/test/material/input_chip_test.dart +++ b/packages/flutter/test/material/input_chip_test.dart @@ -9,6 +9,8 @@ library; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; /// Adds the basic requirements for a Chip. @@ -617,4 +619,77 @@ void main() { expect(tester.widget(find.byType(RawChip)).chipAnimationStyle, chipAnimationStyle); }); + + testWidgets('InputChip mouse cursor behavior', (WidgetTester tester) async { + const SystemMouseCursor customCursor = SystemMouseCursors.grab; + + await tester.pumpWidget(wrapForChip( + child: const Center( + child: InputChip( + mouseCursor: customCursor, + label: Text('Chip'), + ), + ), + )); + + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1); + await gesture.addPointer(location: const Offset(10, 10)); + await tester.pump(); + expect( + RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), + SystemMouseCursors.basic, + ); + + final Offset chip = tester.getCenter(find.text('Chip')); + await gesture.moveTo(chip); + await tester.pump(); + expect( + RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), + customCursor, + ); + }); + + testWidgets('Mouse cursor resolves in focused/unfocused/disabled states', (WidgetTester tester) async { + tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; + final FocusNode focusNode = FocusNode(debugLabel: 'Chip'); + addTearDown(focusNode.dispose); + + Widget buildChip({ required bool enabled }) { + return wrapForChip( + child: Center( + child: InputChip( + mouseCursor: const WidgetStateMouseCursor.fromMap( + { + WidgetState.disabled: SystemMouseCursors.forbidden, + WidgetState.focused: SystemMouseCursors.grab, + WidgetState.selected: SystemMouseCursors.click, + WidgetState.any: SystemMouseCursors.basic, + }, + ), + focusNode: focusNode, + label: const Text('Chip'), + onSelected: enabled ? (bool value) {} : null, + ), + ), + ); + } + + // Unfocused case. + await tester.pumpWidget(buildChip(enabled: true)); + final TestGesture gesture1 = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1); + addTearDown(gesture1.removePointer); + await gesture1.addPointer(location: tester.getCenter(find.text('Chip'))); + await tester.pump(); + await gesture1.moveTo(tester.getCenter(find.text('Chip'))); + expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic); + + // Focused case. + focusNode.requestFocus(); + await tester.pump(); + expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.grab); + + // Disabled case. + await tester.pumpWidget(buildChip(enabled: false)); + expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.forbidden); + }); }