Fix/ios share context menu (#176199)

This PR fixes https://github.com/flutter/flutter/issues/173491 by adding
the missing 'Share' option to the default iOS SystemContextMenu when
shareEnabled is true.

Changes:

Added IOSSystemContextMenuItemShare to getDefaultItems in
system_context_menu.dart.
Added a widget test to ensure Share is present in the default items for
non-empty selections on iOS.
Rationale:
This aligns Flutter's default iOS text selection context menu with
native iOS behavior, ensuring users see the expected 'Share' option when
selecting text.

Demo:
Video showing Share option in iOS context menu:
https://github.com/user-attachments/assets/e04cd1f9-7d92-4147-a09b-719f03d9c625
This commit is contained in:
Md. Murad Hossin 2026-01-07 00:30:13 +06:00 committed by GitHub
parent 59868c551d
commit eb7fb9a3d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 101 additions and 9 deletions

View File

@ -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<IOSSystemContextMenuItem> getDefaultItems(EditableTextState editableTextState) {
return <IOSSystemContextMenuItem>[
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 = <IOSSystemContextMenuItem>[];
// 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

View File

@ -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 = <List<IOSSystemContextMenuItemData>>[];
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.platform,
(MethodCall methodCall) async {
if (methodCall.method == 'ContextMenu.showSystemContextMenu') {
final arguments = methodCall.arguments as Map<String, dynamic>;
final untypedItems = arguments['items'] as List<dynamic>;
final lastItems = <IOSSystemContextMenuItemData>[
for (final dynamic value in untypedItems)
systemContextMenuItemDataFromJson(value as Map<String, dynamic>),
];
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<EditableTextState>(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<IOSSystemContextMenuItemDataShare>()));
},
skip: kIsWeb, // [intended]
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
);
}