Skip to content

Commit 622707c

Browse files
victorsannipull[bot]
authored andcommitted
1 parent 646d1a9 commit 622707c

File tree

4 files changed

+136
-36
lines changed

4 files changed

+136
-36
lines changed

packages/flutter/lib/src/cupertino/context_menu.dart

Lines changed: 72 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import 'package:flutter/widgets.dart';
1616

1717
import 'colors.dart';
1818
import 'localizations.dart';
19+
import 'scrollbar.dart';
1920

2021
// The scale of the child at the time that the CupertinoContextMenu opens.
2122
// This value was eyeballed from a physical device running iOS 13.1.2.
@@ -58,6 +59,11 @@ const Color _borderColor = CupertinoDynamicColor.withBrightness(
5859
darkColor: Color(0xFF57585A),
5960
);
6061

62+
const Color _kBackgroundColor = CupertinoDynamicColor.withBrightness(
63+
color: Color(0xFFF1F1F1),
64+
darkColor: Color(0xFF212122),
65+
);
66+
6167
typedef _DismissCallback = void Function(
6268
BuildContext context,
6369
double scale,
@@ -233,6 +239,10 @@ class CupertinoContextMenu extends StatefulWidget {
233239
static final double animationOpensAt =
234240
_previewLongPressTimeout.inMilliseconds / _animationDuration;
235241

242+
/// The background color of a [CupertinoContextMenuAction] and a
243+
/// [CupertinoContextMenu] sheet.
244+
static const Color kBackgroundColor = _kBackgroundColor;
245+
236246
/// A function that returns a widget to be used alternatively from [child].
237247
///
238248
/// The widget returned by the function will be shown at all times: when the
@@ -1369,21 +1379,36 @@ class _ContextMenuRouteStaticState extends State<_ContextMenuRouteStatic> with T
13691379

13701380
// The menu that displays when CupertinoContextMenu is open. It consists of a
13711381
// list of actions that are typically CupertinoContextMenuActions.
1372-
class _ContextMenuSheet extends StatelessWidget {
1382+
class _ContextMenuSheet extends StatefulWidget {
13731383
_ContextMenuSheet({
13741384
super.key,
13751385
required this.actions,
1376-
required _ContextMenuLocation contextMenuLocation,
1377-
required Orientation orientation,
1378-
}) : assert(actions.isNotEmpty),
1379-
_contextMenuLocation = contextMenuLocation,
1380-
_orientation = orientation;
1386+
required this.contextMenuLocation,
1387+
required this.orientation,
1388+
}) : assert(actions.isNotEmpty);
13811389

13821390
final List<Widget> actions;
1383-
final _ContextMenuLocation _contextMenuLocation;
1384-
final Orientation _orientation;
1391+
final _ContextMenuLocation contextMenuLocation;
1392+
final Orientation orientation;
13851393

1394+
@override
1395+
State<_ContextMenuSheet> createState() => _ContextMenuSheetState();
1396+
}
1397+
1398+
class _ContextMenuSheetState extends State<_ContextMenuSheet> {
1399+
late final ScrollController _controller;
13861400
static const double _kMenuWidth = 250.0;
1401+
// Eyeballed on a context menu on an iOS 15 simulator running iOS 17.5.
1402+
static const double _kScrollbarMainAxisMargin = 13.0;
1403+
1404+
@override
1405+
void initState() {
1406+
super.initState();
1407+
// Link the scrollbar to the scroll view by providing both the same scroll
1408+
// controller. Using SingleChildScrollview.primary might conflict with users
1409+
// already using the PrimaryScrollController.
1410+
_controller = ScrollController();
1411+
}
13871412

13881413
// Get the children, whose order depends on orientation and
13891414
// contextMenuLocation.
@@ -1393,40 +1418,59 @@ class _ContextMenuSheet extends StatelessWidget {
13931418
child: IntrinsicHeight(
13941419
child: ClipRRect(
13951420
borderRadius: const BorderRadius.all(Radius.circular(13.0)),
1396-
child: Column(
1397-
crossAxisAlignment: CrossAxisAlignment.stretch,
1398-
children: <Widget>[
1399-
actions.first,
1400-
for (final Widget action in actions.skip(1))
1401-
DecoratedBox(
1402-
decoration: BoxDecoration(
1403-
border: Border(
1404-
top: BorderSide(
1405-
color: CupertinoDynamicColor.resolve(
1406-
_borderColor,
1407-
context,
1421+
child: ColoredBox(
1422+
color: CupertinoDynamicColor.resolve(CupertinoContextMenu.kBackgroundColor, context),
1423+
child: ScrollConfiguration(
1424+
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
1425+
child: CupertinoScrollbar(
1426+
mainAxisMargin: _kScrollbarMainAxisMargin,
1427+
controller: _controller,
1428+
child: SingleChildScrollView(
1429+
controller: _controller,
1430+
child: Column(
1431+
crossAxisAlignment: CrossAxisAlignment.stretch,
1432+
children: <Widget>[
1433+
widget.actions.first,
1434+
for (final Widget action in widget.actions.skip(1))
1435+
DecoratedBox(
1436+
decoration: BoxDecoration(
1437+
border: Border(
1438+
top: BorderSide(
1439+
color: CupertinoDynamicColor.resolve(
1440+
_borderColor,
1441+
context,
1442+
),
1443+
width: 0.4,
1444+
),
1445+
),
1446+
),
1447+
position: DecorationPosition.foreground,
1448+
child: action,
14081449
),
1409-
width: 0.4,
1410-
),
1411-
),
1450+
],
14121451
),
1413-
position: DecorationPosition.foreground,
1414-
child: action,
14151452
),
1416-
],
1453+
),
1454+
),
14171455
),
14181456
),
14191457
),
14201458
);
14211459

1422-
return switch (_contextMenuLocation) {
1423-
_ContextMenuLocation.center when _orientation == Orientation.portrait => <Widget>[const Spacer(), menu, const Spacer()],
1460+
return switch (widget.contextMenuLocation) {
1461+
_ContextMenuLocation.center when widget.orientation == Orientation.portrait => <Widget>[const Spacer(), menu, const Spacer()],
14241462
_ContextMenuLocation.center => <Widget>[menu, const Spacer()],
14251463
_ContextMenuLocation.right => <Widget>[const Spacer(), menu],
14261464
_ContextMenuLocation.left => <Widget>[menu, const Spacer()],
14271465
};
14281466
}
14291467

1468+
@override
1469+
void dispose() {
1470+
_controller.dispose();
1471+
super.dispose();
1472+
}
1473+
14301474
@override
14311475
Widget build(BuildContext context) {
14321476
return Row(

packages/flutter/lib/src/cupertino/context_menu_action.dart

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import 'package:flutter/foundation.dart';
66
import 'package:flutter/widgets.dart';
77
import 'colors.dart';
8+
import 'context_menu.dart';
89

910
/// A button in a _ContextMenuSheet.
1011
///
@@ -47,10 +48,6 @@ class CupertinoContextMenuAction extends StatefulWidget {
4748
}
4849

4950
class _CupertinoContextMenuActionState extends State<CupertinoContextMenuAction> {
50-
static const Color _kBackgroundColor = CupertinoDynamicColor.withBrightness(
51-
color: Color(0xFFF1F1F1),
52-
darkColor: Color(0xFF212122),
53-
);
5451
static const Color _kBackgroundColorPressed = CupertinoDynamicColor.withBrightness(
5552
color: Color(0xFFDDDDDD),
5653
darkColor: Color(0xFF3F3F40),
@@ -123,7 +120,7 @@ class _CupertinoContextMenuActionState extends State<CupertinoContextMenuAction>
123120
child: ColoredBox(
124121
color: _isPressed
125122
? CupertinoDynamicColor.resolve(_kBackgroundColorPressed, context)
126-
: CupertinoDynamicColor.resolve(_kBackgroundColor, context),
123+
: CupertinoDynamicColor.resolve(CupertinoContextMenu.kBackgroundColor, context),
127124
child: Padding(
128125
padding: const EdgeInsets.fromLTRB(15.5, 8.0, 17.5, 8.0),
129126
child: DefaultTextStyle(

packages/flutter/lib/src/cupertino/scrollbar.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ class CupertinoScrollbar extends RawScrollbar {
8484
this.radiusWhileDragging = defaultRadiusWhileDragging,
8585
ScrollNotificationPredicate? notificationPredicate,
8686
super.scrollbarOrientation,
87+
super.mainAxisMargin = _kScrollbarMainAxisMargin,
8788
}) : assert(thickness < double.infinity),
8889
assert(thicknessWhileDragging < double.infinity),
8990
super(
@@ -156,7 +157,7 @@ class _CupertinoScrollbarState extends RawScrollbarState<CupertinoScrollbar> {
156157
..color = CupertinoDynamicColor.resolve(_kScrollbarColor, context)
157158
..textDirection = Directionality.of(context)
158159
..thickness = _thickness
159-
..mainAxisMargin = _kScrollbarMainAxisMargin
160+
..mainAxisMargin = widget.mainAxisMargin
160161
..crossAxisMargin = _kScrollbarCrossAxisMargin
161162
..radius = _radius
162163
..padding = MediaQuery.paddingOf(context)

packages/flutter/test/cupertino/context_menu_test.dart

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,8 @@ void main() {
495495
await tester.pumpAndSettle();
496496
expect(findStatic(), findsOneWidget);
497497

498-
expect(findStaticChildColor(tester), findsNWidgets(1));
498+
// Both the background color and the action colors are found.
499+
expect(findStaticChildColor(tester), findsNWidgets(2));
499500

500501
// Close the CupertinoContextMenu.
501502
await tester.tapAt(const Offset(1.0, 1.0));
@@ -527,7 +528,7 @@ void main() {
527528
await tester.pumpAndSettle();
528529
expect(findStatic(), findsOneWidget);
529530

530-
expect(findStaticChildColor(tester), findsNWidgets(2));
531+
expect(findStaticChildColor(tester), findsNWidgets(3));
531532
});
532533

533534
testWidgets('Can close CupertinoContextMenu by background tap', (WidgetTester tester) async {
@@ -1024,4 +1025,61 @@ void main() {
10241025
expect(routeStatic, findsOneWidget);
10251026
}
10261027
});
1028+
1029+
testWidgets('CupertinoContextMenu scrolls correctly', (WidgetTester tester) async {
1030+
const int numMenuItems = 100;
1031+
final Widget child = getChild();
1032+
await tester.pumpWidget(
1033+
CupertinoApp(
1034+
home: CupertinoPageScaffold(
1035+
child: MediaQuery(
1036+
data: const MediaQueryData(size: Size(100, 100)),
1037+
child: CupertinoContextMenu(
1038+
actions: List<CupertinoContextMenuAction>.generate(numMenuItems, (int index) {
1039+
return CupertinoContextMenuAction(
1040+
child: Text('Item $index'),
1041+
onPressed: () {},
1042+
);
1043+
}),
1044+
child: child,
1045+
),
1046+
),
1047+
),
1048+
),
1049+
);
1050+
1051+
// Open the CupertinoContextMenu.
1052+
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byWidget(child)));
1053+
await tester.pumpAndSettle();
1054+
await gesture.up();
1055+
await tester.pumpAndSettle();
1056+
1057+
expect(find.byType(CupertinoContextMenu), findsOneWidget);
1058+
1059+
// Verify the first items are visible.
1060+
expect(find.text('Item 0'), findsOneWidget);
1061+
expect(find.text('Item 1'), findsOneWidget);
1062+
1063+
// Find the scrollable part of the context menu.
1064+
final Finder scrollableFinder = find.byType(Scrollable);
1065+
expect(scrollableFinder, findsOneWidget);
1066+
1067+
// Verify a scrollbar is displayed.
1068+
expect(find.byType(CupertinoScrollbar), findsOneWidget);
1069+
1070+
// Scroll to the bottom.
1071+
await tester.drag(scrollableFinder, const Offset(0, -500));
1072+
await tester.pumpAndSettle();
1073+
1074+
// Verify the last item is visible.
1075+
expect(find.text('Item ${numMenuItems - 1}'), findsOneWidget);
1076+
1077+
// Scroll back to the top.
1078+
await tester.drag(scrollableFinder, const Offset(0, 500));
1079+
await tester.pumpAndSettle();
1080+
1081+
// Verify the first items are still visible.
1082+
expect(find.text('Item 0'), findsOneWidget);
1083+
expect(find.text('Item 1'), findsOneWidget);
1084+
});
10271085
}

0 commit comments

Comments
 (0)