Skip to content

Commit 579a9ed

Browse files
authored
feat(ios): add SF Symbol icon support for context menu items (#191)
* feat(ios): add SF Symbol icon support for context menu items * refactor: move optional props to the bottom
1 parent 274ed44 commit 579a9ed

17 files changed

Lines changed: 102 additions & 29 deletions

apps/example/src/App.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
ScrollView,
55
Alert,
66
Linking,
7+
Platform,
78
View,
89
Text,
910
} from 'react-native';
@@ -25,7 +26,8 @@ export default function App() {
2526
const contextMenuItems = useMemo(
2627
() => [
2728
{
28-
text: '✦ Summarize with AI',
29+
text: 'Summarize with AI',
30+
icon: Platform.OS === 'ios' ? 'sparkles' : undefined,
2931
onPress: ({ text }: { text: string }) => {
3032
Alert.alert('✦ Summarize with AI', `"${text}"`, [
3133
{ text: 'Dismiss', style: 'cancel' },
@@ -34,6 +36,7 @@ export default function App() {
3436
},
3537
{
3638
text: 'Translate',
39+
icon: Platform.OS === 'ios' ? 'globe' : undefined,
3740
onPress: ({ text }: { text: string }) => {
3841
Alert.alert('Translate', `"${text}"`, [
3942
{ text: 'Dismiss', style: 'cancel' },

apps/example/src/InputScreen.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ export default function InputScreen() {
107107
const bubbleContextMenuItems = useMemo(
108108
() => [
109109
{
110-
text: '✦ Summarize with AI',
110+
text: 'Summarize with AI',
111+
icon: Platform.OS === 'ios' ? 'sparkles' : undefined,
111112
onPress: ({ text }: { text: string }) => {
112113
Alert.alert('✦ Summarize with AI', `"${text}"`, [
113114
{ text: 'Dismiss', style: 'cancel' },
@@ -116,6 +117,8 @@ export default function InputScreen() {
116117
},
117118
{
118119
text: 'Reply',
120+
icon:
121+
Platform.OS === 'ios' ? 'arrowshape.turn.up.left.fill' : undefined,
119122
onPress: ({ text }: { text: string }) => {
120123
inputRef.current?.setValue(`> ${text}\n\n`);
121124
inputRef.current?.focus();
@@ -129,6 +132,7 @@ export default function InputScreen() {
129132
() => [
130133
{
131134
text: '✦ Summarize with AI',
135+
icon: Platform.OS === 'ios' ? 'sparkles' : undefined,
132136
onPress: ({
133137
text,
134138
styleState,

apps/macos-example/src/App.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ export default function App() {
2525
const contextMenuItems = useMemo(
2626
() => [
2727
{
28-
text: '✦ Summarize with AI',
28+
text: 'Summarize with AI',
29+
icon: 'sparkles',
2930
onPress: ({ text }: { text: string }) => {
3031
Alert.alert('✦ Summarize with AI', `"${text}"`, [
3132
{ text: 'Dismiss', style: 'cancel' },
@@ -34,6 +35,7 @@ export default function App() {
3435
},
3536
{
3637
text: 'Translate',
38+
icon: 'globe',
3739
onPress: ({ text }: { text: string }) => {
3840
Alert.alert('Translate', `"${text}"`, [
3941
{ text: 'Dismiss', style: 'cancel' },

apps/macos-example/src/InputScreen.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ export default function InputScreen() {
6666
const bubbleContextMenuItems = useMemo(
6767
() => [
6868
{
69-
text: '✦ Summarize with AI',
69+
text: 'Summarize with AI',
70+
icon: 'sparkles',
7071
onPress: ({ text }: { text: string }) => {
7172
Alert.alert('✦ Summarize with AI', `"${text}"`, [
7273
{ text: 'Dismiss', style: 'cancel' },
@@ -75,6 +76,7 @@ export default function InputScreen() {
7576
},
7677
{
7778
text: 'Reply',
79+
icon: 'arrowshape.turn.up.left.fill',
7880
onPress: ({ text }: { text: string }) => {
7981
inputRef.current?.setValue(`> ${text}\n\n`);
8082
inputRef.current?.focus();
@@ -87,7 +89,8 @@ export default function InputScreen() {
8789
const inputContextMenuItems = useMemo(
8890
() => [
8991
{
90-
text: '✦ Summarize with AI',
92+
text: 'Summarize with AI',
93+
icon: 'sparkles',
9194
onPress: ({
9295
text,
9396
styleState,

docs/API_REFERENCE.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,8 @@ Markdown flavor. Set to `'github'` to enable GitHub Flavored Markdown table supp
172172

173173
Custom items to add to the text selection context menu. Items appear before the system actions (Copy, etc.). Items with `visible: false` are hidden from the menu.
174174

175+
> **iOS**: Requires iOS 16+. On earlier versions the prop is ignored.
176+
175177
| Type | Default Value | Platform |
176178
| -------------------- | ------------- | -------- |
177179
| `ContextMenuItem[]` | - | Both |
@@ -182,6 +184,12 @@ Custom items to add to the text selection context menu. Items appear before the
182184
interface ContextMenuItem {
183185
/** Label shown in the context menu. */
184186
text: string;
187+
/**
188+
* SF Symbol name for the icon shown next to the item label.
189+
* Supported on iOS and macOS. Ignored on Android.
190+
* Example: 'sparkles', 'translate', 'doc.text'
191+
*/
192+
icon?: string;
185193
/** Called when the item is tapped. */
186194
onPress: (event: {
187195
/** The selected text at the time of the press. */
@@ -393,6 +401,8 @@ Fires when the input loses focus.
393401

394402
Custom items to add to the text selection context menu. Items appear before the system actions (Copy, Cut, etc.). Items with `visible: false` are hidden from the menu.
395403

404+
> **iOS**: Requires iOS 16+. On earlier versions the prop is ignored.
405+
396406
| Type | Default Value | Platform |
397407
| -------------------- | ------------- | -------- |
398408
| `ContextMenuItem[]` | - | Both |
@@ -403,6 +413,12 @@ Custom items to add to the text selection context menu. Items appear before the
403413
interface ContextMenuItem {
404414
/** Label shown in the context menu. */
405415
text: string;
416+
/**
417+
* SF Symbol name for the icon shown next to the item label.
418+
* Supported on iOS and macOS. Ignored on Android.
419+
* Example: 'sparkles', 'translate', 'doc.text'
420+
*/
421+
icon?: string;
406422
/** Called when the item is tapped. */
407423
onPress: (event: {
408424
/** The selected text at the time of the press. */

ios/EnrichedMarkdown.mm

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ @implementation EnrichedMarkdown {
140140
BOOL _enableLinkPreview;
141141

142142
NSArray<NSString *> *_contextMenuItemTexts;
143+
NSArray<NSString *> *_contextMenuItemIcons;
143144
}
144145

145146
+ (ComponentDescriptorProvider)componentDescriptorProvider
@@ -545,7 +546,7 @@ - (EnrichedMarkdownInternalText *)createTextViewForRenderedSegment:(EMRenderedTe
545546
}
546547
NSString *segmentMarkdown = extractMarkdownFromAttributedString(textView.textStorage, textView.selectedRange);
547548
NSArray<NSMenuItem *> *customItems = ENRMBuildContextMenuItems(
548-
strongSelf->_contextMenuItemTexts, textView,
549+
strongSelf->_contextMenuItemTexts, strongSelf->_contextMenuItemIcons, textView,
549550
^(NSString *itemText, NSString *selectedText, NSUInteger selectionStart, NSUInteger selectionEnd) {
550551
auto eventEmitter = std::static_pointer_cast<EnrichedMarkdownEventEmitter const>(strongSelf->_eventEmitter);
551552
if (eventEmitter) {
@@ -683,6 +684,7 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
683684

684685
if (ENRMContextMenuItemsChanged(oldViewProps.contextMenuItems, newViewProps.contextMenuItems)) {
685686
_contextMenuItemTexts = ENRMContextMenuTextsFromItems(newViewProps.contextMenuItems);
687+
_contextMenuItemIcons = ENRMContextMenuIconsFromItems(newViewProps.contextMenuItems);
686688
}
687689

688690
if (markdownChanged || stylePropChanged || md4cFlagsChanged || allowTrailingMarginChanged) {
@@ -812,7 +814,7 @@ - (UIMenu *)textView:(UITextView *)textView
812814
}
813815
};
814816
NSMutableArray<UIAction *> *customActions =
815-
ENRMBuildContextMenuActions(_contextMenuItemTexts, textView, range, handler);
817+
ENRMBuildContextMenuActions(_contextMenuItemTexts, _contextMenuItemIcons, textView, range, handler);
816818

817819
NSString *segmentMarkdown = extractMarkdownFromAttributedString(textView.attributedText, range);
818820
return buildEditMenuForSelection(textView.attributedText, range, segmentMarkdown, _config, suggestedActions,

ios/EnrichedMarkdownText.mm

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ @implementation EnrichedMarkdownText {
8282
BOOL _accessibilityNeedsRebuild;
8383

8484
NSArray<NSString *> *_contextMenuItemTexts;
85+
NSArray<NSString *> *_contextMenuItemIcons;
8586
}
8687

8788
+ (ComponentDescriptorProvider)componentDescriptorProvider
@@ -210,7 +211,7 @@ - (void)setupTextView
210211
return baseMenu;
211212
}
212213
NSArray<NSMenuItem *> *customItems = ENRMBuildContextMenuItems(
213-
strongSelf->_contextMenuItemTexts, textView,
214+
strongSelf->_contextMenuItemTexts, strongSelf->_contextMenuItemIcons, textView,
214215
^(NSString *itemText, NSString *selectedText, NSUInteger selectionStart, NSUInteger selectionEnd) {
215216
auto eventEmitter =
216217
std::static_pointer_cast<EnrichedMarkdownTextEventEmitter const>(strongSelf->_eventEmitter);
@@ -495,6 +496,7 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
495496

496497
if (ENRMContextMenuItemsChanged(oldViewProps.contextMenuItems, newViewProps.contextMenuItems)) {
497498
_contextMenuItemTexts = ENRMContextMenuTextsFromItems(newViewProps.contextMenuItems);
499+
_contextMenuItemIcons = ENRMContextMenuIconsFromItems(newViewProps.contextMenuItems);
498500
}
499501

500502
if (newViewProps.streamingAnimation != oldViewProps.streamingAnimation) {
@@ -629,7 +631,7 @@ - (UIMenu *)textView:(ENRMPlatformTextView *)textView
629631
}
630632
};
631633
NSMutableArray<UIAction *> *customActions =
632-
ENRMBuildContextMenuActions(_contextMenuItemTexts, textView, range, handler);
634+
ENRMBuildContextMenuActions(_contextMenuItemTexts, _contextMenuItemIcons, textView, range, handler);
633635

634636
return buildEditMenuForSelection(textView.attributedText, range, _cachedMarkdown, _config, suggestedActions,
635637
customActions);

ios/input/EnrichedMarkdownInput+ContextMenu.mm

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,19 @@ - (UIMenu *)textView:(UITextView *)textView
3737
handler:^(__kindof UIAction *action) { [self copySelectedRangeAsMarkdown]; }];
3838

3939
NSArray<NSString *> *customItemTexts = [self contextMenuItemTexts];
40+
NSArray<NSString *> *customItemIcons = [self contextMenuItemIcons];
4041
__weak EnrichedMarkdownInput *weakSelf = self;
4142
NSMutableArray<UIMenuElement *> *allActions = [NSMutableArray arrayWithCapacity:customItemTexts.count];
42-
for (NSString *itemText in customItemTexts) {
43+
[customItemTexts enumerateObjectsUsingBlock:^(NSString *itemText, NSUInteger index, BOOL *_) {
44+
NSString *iconName = index < customItemIcons.count ? customItemIcons[index] : nil;
45+
UIImage *image = iconName.length > 0 ? [UIImage systemImageNamed:iconName] : nil;
4346
UIAction *customAction =
4447
[UIAction actionWithTitle:itemText
45-
image:nil
48+
image:image
4649
identifier:nil
4750
handler:^(__kindof UIAction *_) { [weakSelf emitContextMenuItemPress:itemText]; }];
4851
[allActions addObject:customAction];
49-
}
52+
}];
5053

5154
NSUInteger insertIndex = suggestedActions.count;
5255
NSMutableArray *systemActions = [suggestedActions mutableCopy];
@@ -70,10 +73,11 @@ - (NSMenu *)enrichedMenuForEvent:(NSEvent *)event defaultMenu:(NSMenu *)menu tex
7073
}
7174

7275
__weak EnrichedMarkdownInput *weakSelf = self;
73-
NSArray<NSMenuItem *> *customItems = ENRMBuildContextMenuItems(
74-
[self contextMenuItemTexts], textView, ^(NSString *itemText, NSString *_, NSUInteger __, NSUInteger ___) {
75-
[weakSelf emitContextMenuItemPress:itemText];
76-
});
76+
NSArray<NSMenuItem *> *customItems =
77+
ENRMBuildContextMenuItems([self contextMenuItemTexts], [self contextMenuItemIcons], textView,
78+
^(NSString *itemText, NSString *_, NSUInteger __, NSUInteger ___) {
79+
[weakSelf emitContextMenuItemPress:itemText];
80+
});
7781
ENRMPrependMenuItems(menu, customItems);
7882

7983
[menu addItem:[NSMenuItem separatorItem]];

ios/input/EnrichedMarkdownInput+Internal.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ NS_ASSUME_NONNULL_BEGIN
1414

1515
- (void)emitContextMenuItemPress:(NSString *)itemText;
1616
- (NSArray<NSString *> *)contextMenuItemTexts;
17+
- (NSArray<NSString *> *)contextMenuItemIcons;
1718

1819
#if !TARGET_OS_OSX
1920
- (void)showFormatBar;

ios/input/EnrichedMarkdownInput.mm

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ @implementation EnrichedMarkdownInput {
7373
#endif
7474

7575
NSArray<NSString *> *_contextMenuItemTexts;
76+
NSArray<NSString *> *_contextMenuItemIcons;
7677
}
7778

7879
#pragma mark - Fabric lifecycle
@@ -277,6 +278,7 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
277278

278279
if (ENRMContextMenuItemsChanged(oldViewProps.contextMenuItems, newViewProps.contextMenuItems)) {
279280
_contextMenuItemTexts = ENRMContextMenuTextsFromItems(newViewProps.contextMenuItems);
281+
_contextMenuItemIcons = ENRMContextMenuIconsFromItems(newViewProps.contextMenuItems);
280282
}
281283

282284
BOOL styleChanged = applyInputStyleProps(_formatterStyle, newViewProps, oldViewProps);
@@ -824,6 +826,11 @@ - (void)emitOnChangeState
824826
return _contextMenuItemTexts ?: @[];
825827
}
826828

829+
- (NSArray<NSString *> *)contextMenuItemIcons
830+
{
831+
return _contextMenuItemIcons ?: @[];
832+
}
833+
827834
- (void)emitContextMenuItemPress:(NSString *)itemText
828835
{
829836
auto eventEmitter = [self getEventEmitter];

0 commit comments

Comments
 (0)