diff --git a/packages/flutter/lib/src/widgets/system_context_menu.dart b/packages/flutter/lib/src/widgets/system_context_menu.dart index c8d9423d581..b4b1f193a2a 100644 --- a/packages/flutter/lib/src/widgets/system_context_menu.dart +++ b/packages/flutter/lib/src/widgets/system_context_menu.dart @@ -10,6 +10,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'basic.dart'; +import 'context_menu_button_item.dart'; import 'editable_text.dart'; import 'framework.dart'; import 'localizations.dart'; @@ -156,15 +157,36 @@ class SystemContextMenu extends StatefulWidget { /// * [EditableTextState.contextMenuButtonItems], which provides the default /// [ContextMenuButtonItem]s for the Flutter-rendered context menu. static List getDefaultItems(EditableTextState editableTextState) { - return [ - if (editableTextState.copyEnabled) const IOSSystemContextMenuItemCopy(), - if (editableTextState.cutEnabled) const IOSSystemContextMenuItemCut(), - if (editableTextState.pasteEnabled) const IOSSystemContextMenuItemPaste(), - if (editableTextState.selectAllEnabled) const IOSSystemContextMenuItemSelectAll(), - if (editableTextState.lookUpEnabled) const IOSSystemContextMenuItemLookUp(), - if (editableTextState.searchWebEnabled) const IOSSystemContextMenuItemSearchWeb(), - if (editableTextState.liveTextInputEnabled) const IOSSystemContextMenuItemLiveText(), - ]; + final items = []; + + // Use the generic Flutter-rendered context menu model as the single source of truth. + for (final ContextMenuButtonItem button in editableTextState.contextMenuButtonItems) { + switch (button.type) { + case ContextMenuButtonType.copy: + items.add(const IOSSystemContextMenuItemCopy()); + case ContextMenuButtonType.cut: + items.add(const IOSSystemContextMenuItemCut()); + case ContextMenuButtonType.paste: + items.add(const IOSSystemContextMenuItemPaste()); + case ContextMenuButtonType.selectAll: + items.add(const IOSSystemContextMenuItemSelectAll()); + case ContextMenuButtonType.lookUp: + items.add(const IOSSystemContextMenuItemLookUp()); + case ContextMenuButtonType.searchWeb: + items.add(const IOSSystemContextMenuItemSearchWeb()); + case ContextMenuButtonType.share: + items.add(const IOSSystemContextMenuItemShare()); + case ContextMenuButtonType.liveTextInput: + items.add(const IOSSystemContextMenuItemLiveText()); + case ContextMenuButtonType.delete: + // No native iOS system menu button for Delete — intentionally ignored. + case ContextMenuButtonType.custom: + // Custom items are provided explicitly via SystemContextMenu.items, + // not via defaults. Intentionally ignore in default mapping. + } + } + + return items; } @override diff --git a/packages/flutter/test/widgets/system_context_menu_test.dart b/packages/flutter/test/widgets/system_context_menu_test.dart index ef86eb8aa96..b75c976f10f 100644 --- a/packages/flutter/test/widgets/system_context_menu_test.dart +++ b/packages/flutter/test/widgets/system_context_menu_test.dart @@ -1332,4 +1332,74 @@ void main() { skip: kIsWeb, // [intended] variant: TargetPlatformVariant.only(TargetPlatform.iOS), ); + + testWidgets( + 'Default iOS SystemContextMenu includes Share for non-empty selection', + (WidgetTester tester) async { + final itemsReceived = >[]; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( + SystemChannels.platform, + (MethodCall methodCall) async { + if (methodCall.method == 'ContextMenu.showSystemContextMenu') { + final arguments = methodCall.arguments as Map; + final untypedItems = arguments['items'] as List; + final lastItems = [ + for (final dynamic value in untypedItems) + systemContextMenuItemDataFromJson(value as Map), + ]; + itemsReceived.add(lastItems); + } + return; + }, + ); + addTearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( + SystemChannels.platform, + null, + ); + }); + + final controller = TextEditingController(text: 'Hello world'); + addTearDown(controller.dispose); + await tester.pumpWidget( + MediaQuery( + data: const MediaQueryData(supportsShowingSystemContextMenu: true), + child: MaterialApp( + home: Scaffold( + body: Center( + child: TextField( + controller: controller, + contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) { + return SystemContextMenu.editableText(editableTextState: editableTextState); + }, + ), + ), + ), + ), + ), + ); + + // Focus the field first (this establishes the TextInputConnection). + await tester.tap(find.byType(TextField)); + await tester.pump(); + final EditableTextState state = tester.state(find.byType(EditableText)); + + // Set a non-empty selection to enable sharing. + controller.selection = const TextSelection(baseOffset: 0, extentOffset: 5); // "Hello" + await tester.pump(); + + // Nit: ensure no platform message sent before showing toolbar. + expect(itemsReceived, isEmpty); + + // Show the context menu. + expect(state.showToolbar(), true); + await tester.pump(); + + // Assert that the platform message included a Share item. + expect(itemsReceived, isNotEmpty); + expect(itemsReceived.last, contains(isA())); + }, + skip: kIsWeb, // [intended] + variant: TargetPlatformVariant.only(TargetPlatform.iOS), + ); }