diff --git a/packages/flutter/lib/src/cupertino/context_menu.dart b/packages/flutter/lib/src/cupertino/context_menu.dart index 594c6e561e..f997978b98 100644 --- a/packages/flutter/lib/src/cupertino/context_menu.dart +++ b/packages/flutter/lib/src/cupertino/context_menu.dart @@ -4,6 +4,7 @@ import 'dart:math' as math; import 'dart:ui' as ui; +import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart' show kMinFlingVelocity, kLongPressTimeout; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; @@ -367,17 +368,20 @@ class _CupertinoContextMenuState extends State with Ticker @override Widget build(BuildContext context) { - return GestureDetector( - onTapCancel: _onTapCancel, - onTapDown: _onTapDown, - onTapUp: _onTapUp, - onTap: _onTap, - child: TickerMode( - enabled: !_childHidden, - child: Opacity( - key: _childGlobalKey, - opacity: _childHidden ? 0.0 : 1.0, - child: widget.child, + return MouseRegion( + cursor: kIsWeb ? SystemMouseCursors.click : MouseCursor.defer, + child: GestureDetector( + onTapCancel: _onTapCancel, + onTapDown: _onTapDown, + onTapUp: _onTapUp, + onTap: _onTap, + child: TickerMode( + enabled: !_childHidden, + child: Opacity( + key: _childGlobalKey, + opacity: _childHidden ? 0.0 : 1.0, + child: widget.child, + ), ), ), ); diff --git a/packages/flutter/lib/src/cupertino/context_menu_action.dart b/packages/flutter/lib/src/cupertino/context_menu_action.dart index c0dae6b8dc..1b84b357f1 100644 --- a/packages/flutter/lib/src/cupertino/context_menu_action.dart +++ b/packages/flutter/lib/src/cupertino/context_menu_action.dart @@ -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/foundation.dart'; import 'package:flutter/widgets.dart'; import 'colors.dart'; @@ -106,43 +107,46 @@ class _CupertinoContextMenuActionState extends State @override Widget build(BuildContext context) { - return GestureDetector( - key: _globalKey, - onTapDown: onTapDown, - onTapUp: onTapUp, - onTapCancel: onTapCancel, - onTap: widget.onPressed, - behavior: HitTestBehavior.opaque, - child: ConstrainedBox( - constraints: const BoxConstraints( - minHeight: _kButtonHeight, - ), - child: Semantics( - button: true, - child: Container( - decoration: BoxDecoration( - color: _isPressed - ? CupertinoDynamicColor.resolve(_kBackgroundColorPressed, context) - : CupertinoDynamicColor.resolve(_kBackgroundColor, context), - ), - padding: const EdgeInsets.symmetric( - vertical: 16.0, - horizontal: 10.0, - ), - child: DefaultTextStyle( - style: _textStyle, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: widget.child, - ), - if (widget.trailingIcon != null) - Icon( - widget.trailingIcon, - color: _textStyle.color, + return MouseRegion( + cursor: widget.onPressed != null && kIsWeb ? SystemMouseCursors.click : MouseCursor.defer, + child: GestureDetector( + key: _globalKey, + onTapDown: onTapDown, + onTapUp: onTapUp, + onTapCancel: onTapCancel, + onTap: widget.onPressed, + behavior: HitTestBehavior.opaque, + child: ConstrainedBox( + constraints: const BoxConstraints( + minHeight: _kButtonHeight, + ), + child: Semantics( + button: true, + child: Container( + decoration: BoxDecoration( + color: _isPressed + ? CupertinoDynamicColor.resolve(_kBackgroundColorPressed, context) + : CupertinoDynamicColor.resolve(_kBackgroundColor, context), + ), + padding: const EdgeInsets.symmetric( + vertical: 16.0, + horizontal: 10.0, + ), + child: DefaultTextStyle( + style: _textStyle, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: widget.child, ), - ], + if (widget.trailingIcon != null) + Icon( + widget.trailingIcon, + color: _textStyle.color, + ), + ], + ), ), ), ), diff --git a/packages/flutter/test/cupertino/context_menu_action_test.dart b/packages/flutter/test/cupertino/context_menu_action_test.dart index f8ed139c8b..0b11060555 100644 --- a/packages/flutter/test/cupertino/context_menu_action_test.dart +++ b/packages/flutter/test/cupertino/context_menu_action_test.dart @@ -3,6 +3,9 @@ // found in the LICENSE file. import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import '../rendering/mock_canvas.dart'; @@ -122,4 +125,29 @@ void main() { expect(_getTextStyle(tester).fontWeight, kDefaultActionWeight); }); + testWidgets( + 'Hovering over Cupertino context menu action updates cursor to clickable on Web', + (WidgetTester tester) async { + /// Cupertino context menu action without "onPressed" callback. + await tester.pumpWidget(_getApp()); + final Offset contextMenuAction = tester.getCenter(find.text('I am a CupertinoContextMenuAction')); + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1); + await gesture.addPointer(location: contextMenuAction); + await tester.pumpAndSettle(); + expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic); + + // / Cupertino context menu action with "onPressed" callback. + await tester.pumpWidget(_getApp(onPressed: (){})); + await gesture.moveTo(const Offset(10, 10)); + await tester.pumpAndSettle(); + expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic); + + await gesture.moveTo(contextMenuAction); + addTearDown(gesture.removePointer); + await tester.pumpAndSettle(); + expect( + RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), + kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic, + ); + }); } diff --git a/packages/flutter/test/cupertino/context_menu_test.dart b/packages/flutter/test/cupertino/context_menu_test.dart index 2a4e0759a8..2cf6878937 100644 --- a/packages/flutter/test/cupertino/context_menu_test.dart +++ b/packages/flutter/test/cupertino/context_menu_test.dart @@ -3,6 +3,9 @@ // found in the LICENSE file. import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { @@ -193,6 +196,38 @@ void main() { await tester.pumpAndSettle(); expect(_findStatic(), findsOneWidget); }); + + testWidgets('Hovering over Cupertino context menu updates cursor to clickable on Web', (WidgetTester tester) async { + final Widget child = _getChild(); + await tester.pumpWidget(CupertinoApp( + home: CupertinoPageScaffold( + child: Center( + child: CupertinoContextMenu( + actions: const [ + CupertinoContextMenuAction( + child: Text('CupertinoContextMenuAction One'), + ), + ], + child: child, + ), + ), + ), + )); + + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1); + await gesture.addPointer(location: const Offset(10, 10)); + await tester.pumpAndSettle(); + expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic); + + final Offset contextMenu = tester.getCenter(find.byWidget(child)); + await gesture.moveTo(contextMenu); + addTearDown(gesture.removePointer); + await tester.pumpAndSettle(); + expect( + RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), + kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic, + ); + }); }); group('CupertinoContextMenu when open', () {