Skip to content

Commit bea0a1c

Browse files
authored
feat(iOS): add customIcon and customIconName properties to the action object (#111)
* Add customIcon and customIconColor properties to ContextMenuAction * Convert the new ContextMenuAction properties * Set customIcon with customIconColor, if available, for the ContextMenu actions * Add type declarations for new props * Update README with new action props, fix other typos * Add example for new action props * Update readme and specify the higher priority of customIcon * Refactor icon and iconColor props on iOS native side * Remove "system" prefix from icon and iconColor on Android native side * Update type declarations with new the new prop changes * Update and format README * Call processColor for iOS in index.js
1 parent a276222 commit bea0a1c

File tree

11 files changed

+118
-37
lines changed

11 files changed

+118
-37
lines changed

README.md

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -42,48 +42,56 @@ See `example/` for basic usage.
4242

4343
## Props
4444

45-
###### `title`
45+
### `title`
4646

4747
Optional. The title above the popup menu.
4848

49-
###### `actions`
49+
### `actions`
5050

51-
Array of `{ title: string, subtitle?: string, systemIcon?: string, systemIconColor?: string, destructive?: boolean, selected?: boolean, disabled?: boolean, disabled?: boolean, inlineChildren?: boolean, actions?: Array<ContextMenuAction> }`.
51+
Array of `{ title: string, subtitle?: string, systemIcon?: string, icon?: string, iconColor?: string, destructive?: boolean, selected?: boolean, disabled?: boolean, disabled?: boolean, inlineChildren?: boolean, actions?: Array<ContextMenuAction> }`.
5252

53-
Subtitle is only available on iOS 15+.
53+
- `title` is the title of the action
5454

55-
System icon refers to an icon name within [SF Symbols](https://developer.apple.com/design/human-interface-guidelines/sf-symbols/overview/) on IOS and Drawable name on Android.
55+
- `subtitle` is the subtitle of the action (iOS 15+ only)
5656

57-
System icon color is only available on Android.
57+
- `systemIcon` refers to an icon name within [SF Symbols](https://developer.apple.com/design/human-interface-guidelines/sf-symbols/overview/) (iOS only)
5858

59-
Destructive items are rendered in red.
59+
- `icon` refers to an SVG asset name that is provided in Assets.xcassets or to a Drawable on Android; when both `systemIcon` and `icon` are provided, `icon` will take a higher priority and it will override `systemIcon`
6060

61-
Selected items have a checkmark next to them on iOS, and unchanged on Android.
61+
- `iconColor` will change the color of the icon provided to the `icon` prop and has no effect on `systemIcon` (default: black)
6262

63-
Menus can be nested one level deep. On iOS submenus can be rendered inline optionally.
63+
- `destructive` items are rendered in red (iOS only, default: false)
6464

65-
###### `onPress`
65+
- `selected` items have a checkmark next to them (iOS only, default: false)
66+
67+
- `disabled` marks whether the action is disabled or not (default: false)
68+
69+
- `actions` will provide a one level deep nested menu; when child actions are supplied, the child's callback will contain its name but the same index as the topmost parent menu/action index
70+
71+
- `inlineChildren` marks whether its children (if any) should be rendered inline instead of in their own child menu (iOS only, default: false)
72+
73+
### `onPress`
6674

6775
Optional. When the popup is opened and the user picks an option. Called with `{ nativeEvent: { index, indexPath, name } }`. When a nested action is selected the top level parent index is used for the callback.
6876

69-
To get the full path to the item, `indexPath` is an array of indices to reach the item. For a top-levle item, it'll be an array with a single index. For an item one deep, it'll be an array with two indicies.
77+
To get the full path to the item, `indexPath` is an array of indices to reach the item. For a top-level item, it'll be an array with a single index. For an item one deep, it'll be an array with two indexes.
7078

71-
###### `onPreviewPress`
79+
### `onPreviewPress`
7280

7381
Optional, iOS only. When the context menu preview is tapped.
7482

75-
###### `onCancel`
83+
### `onCancel`
7684

77-
Optional. When the popop is opened and the user cancels.
85+
Optional. When the popup is opened and the user cancels.
7886

79-
###### `previewBackgroundColor`
87+
### `previewBackgroundColor`
8088

8189
Optional. The background color of the preview. This is displayed underneath your view. Set this to transparent (or another color) if the default causes issues.
8290

83-
###### `dropdownMenuMode`
91+
### `dropdownMenuMode`
8492

8593
Optional. When set to `true`, the context menu is triggered with a single tap instead of a long press, and a preview is not show and no blur occurs. Uses the iOS 14 Menu API on iOS and a simple tap listener on android.
8694

87-
###### `disabled`
95+
### `disabled`
8896

8997
Optional. Disable menu interaction.

android/src/main/java/com/mpiannucci/reactnativecontextmenu/ContextMenuView.java

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,8 @@ private void createContextMenuSubMenu(Menu menu, ReadableMap action, ReadableArr
123123
String title = action.getString("title");
124124
Menu parentMenu = menu.addSubMenu(title);
125125

126-
@Nullable Drawable systemIcon = getResourceWithName(getContext(), action.getString("systemIcon"));
127-
menu.getItem(i).setIcon(systemIcon); // set icon to current item.
126+
@Nullable Drawable icon = getResourceWithName(getContext(), action.getString("icon"));
127+
menu.getItem(i).setIcon(icon); // set icon to current item.
128128

129129
for (int j = 0; j < childActions.size(); j++) {
130130
createContextMenuAction(parentMenu, childActions.getMap(j), j, i);
@@ -135,16 +135,16 @@ private void createContextMenuSubMenu(Menu menu, ReadableMap action, ReadableArr
135135

136136
private void createContextMenuAction(Menu menu, ReadableMap action, int i, int parentIndex) {
137137
String title = action.getString("title");
138-
@Nullable Drawable systemIcon = getResourceWithName(getContext(), action.getString("systemIcon"));
138+
@Nullable Drawable icon = getResourceWithName(getContext(), action.getString("icon"));
139139

140140
MenuItem item = menu.add(Menu.NONE, Menu.NONE, i, title);
141141
item.setEnabled(!action.hasKey("disabled") || !action.getBoolean("disabled"));
142142

143-
if (action.hasKey("systemIconColor") && systemIcon != null) {
144-
int color = Color.parseColor(action.getString("systemIconColor"));
145-
systemIcon.setTint(color);
143+
if (action.hasKey("iconColor") && icon != null) {
144+
int color = Color.parseColor(action.getString("iconColor"));
145+
icon.setTint(color);
146146
}
147-
item.setIcon(systemIcon);
147+
item.setIcon(icon);
148148
if (action.hasKey("destructive") && action.getBoolean("destructive")) {
149149
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
150150
item.setIconTintList(ColorStateList.valueOf(Color.RED));
@@ -185,12 +185,12 @@ private void setMenuIconDisplay(Menu contextMenu, boolean display) {
185185
} catch (Exception ignored) {}
186186
}
187187

188-
private Drawable getResourceWithName(Context context, @Nullable String systemIcon) {
189-
if (systemIcon == null)
188+
private Drawable getResourceWithName(Context context, @Nullable String icon) {
189+
if (icon == null)
190190
return null;
191191

192192
Resources resources = context.getResources();
193-
int resourceId = resources.getIdentifier(systemIcon, "drawable", context.getPackageName());
193+
int resourceId = resources.getIdentifier(icon, "drawable", context.getPackageName());
194194
try {
195195
return resourceId != 0 ? ResourcesCompat.getDrawable(resources, resourceId, context.getTheme()) : null;
196196
} catch (Exception e) {

example/App.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useState } from 'react';
2-
import { SafeAreaView, View, StyleSheet, Platform, TouchableOpacity, Alert } from 'react-native';
2+
import { SafeAreaView, View, StyleSheet, Platform, TouchableOpacity, Alert, processColor } from 'react-native';
33
import ContextMenu from 'react-native-context-menu-view';
44

55
const Icons = Platform.select({
@@ -43,6 +43,11 @@ const App = () => {
4343
systemIcon: Icons.transparent,
4444
destructive: true,
4545
},
46+
{
47+
title: 'Custom Icon and Color',
48+
customIcon: Platform.OS === 'ios' ? 'bluetooth' : '',
49+
customIconColor: Platform.OS === 'ios' ? processColor('green') : '',
50+
},
4651
{
4752
title: 'Toggle Circle',
4853
systemIcon: Icons.toggleCircle,
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"info" : {
3+
"author" : "xcode",
4+
"version" : 1
5+
}
6+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"images" : [
3+
{
4+
"filename" : "bluetooth.svg",
5+
"idiom" : "universal",
6+
"scale" : "1x"
7+
},
8+
{
9+
"idiom" : "universal",
10+
"scale" : "2x"
11+
},
12+
{
13+
"idiom" : "universal",
14+
"scale" : "3x"
15+
}
16+
],
17+
"info" : {
18+
"author" : "xcode",
19+
"version" : 1
20+
}
21+
}
Lines changed: 7 additions & 0 deletions
Loading

index.d.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import React, { Component } from "react";
2-
import { NativeSyntheticEvent, ViewProps, ViewStyle } from "react-native";
2+
import {
3+
NativeSyntheticEvent,
4+
ViewProps,
5+
ViewStyle,
6+
ProcessedColorValue,
7+
} from "react-native";
38

49
export interface ContextMenuAction {
510
/**
@@ -11,13 +16,17 @@ export interface ContextMenuAction {
1116
*/
1217
subtitle?: string;
1318
/**
14-
* The icon to use. This is the name of the SFSymbols icon to use on IOS and name of the Drawable to use on Android.
19+
* The system icon to use. This is the name of the SFSymbols icon (iOS only).
1520
*/
1621
systemIcon?: string;
1722
/**
18-
* Color of icon. (Android only)
23+
* The icon to use. This is the name of the SVG that is provided in Assets.xcassets (iOS) or the name of the Drawable (Android). It overrides the systemIcon prop.
1924
*/
20-
systemIconColor?: string;
25+
icon?: string;
26+
/**
27+
* Color of the icon (default: black). The color only applies to the icon provided to the icon prop, as the color of the systemIcon is always black and cannot be changed with this prop.
28+
*/
29+
iconColor?: string;
2130
/**
2231
* Destructive items are rendered in red on iOS, and unchanged on Android. (default: false)
2332
*/
@@ -35,7 +44,7 @@ export interface ContextMenuAction {
3544
*/
3645
inlineChildren?: boolean;
3746
/**
38-
* Child actions. When child actions are supplied, the childs callback will contain its name but the same index as the topmost parent menu/action index
47+
* Child actions. When child actions are supplied, the child's callback will contain its name but the same index as the topmost parent menu/action index
3948
*/
4049
actions?: Array<ContextMenuAction>;
4150
}

index.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import React from "react";
2-
import { requireNativeComponent, View, Platform, StyleSheet } from "react-native";
2+
import { requireNativeComponent, View, Platform, StyleSheet, processColor } from "react-native";
33

44
const NativeContextMenu = requireNativeComponent("ContextMenu", null);
55

66
const ContextMenu = (props) => {
7+
const iconColor = props?.iconColor
8+
? Platform.OS === 'ios'
9+
? processColor(props.iconColor)
10+
: props.iconColor
11+
: undefined;
12+
713
return (
8-
<NativeContextMenu {...props}>
14+
<NativeContextMenu {...props} iconColor={iconColor}>
915
{props.children}
1016
{props.preview != null && Platform.OS === 'ios' ? (
1117
<View style={styles.preview} nativeID="ContextMenuPreview">{props.preview}</View>

ios/ContextMenuAction.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
@property (nonnull, nonatomic, copy) NSString* title;
1414
@property (nonnull, nonatomic, copy) NSString* subtitle;
1515
@property (nullable, nonatomic, copy) NSString* systemIcon;
16+
@property (nullable, nonatomic, copy) NSString* icon;
17+
@property (nullable, nonatomic, copy) UIColor* iconColor;
1618
@property (nonatomic, assign) BOOL destructive;
1719
@property (nonatomic, assign) BOOL selected;
1820
@property (nonatomic, assign) BOOL disabled;

ios/ContextMenuView.m

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,21 @@ - (UITargetedPreview *)contextMenuInteraction:(UIContextMenuInteraction *)intera
120120

121121
- (UIMenuElement*) createMenuElementForAction:(ContextMenuAction *)action atIndexPath:(NSArray<NSNumber *> *)indexPath {
122122
UIMenuElement* menuElement = nil;
123+
UIImage *iconImage = nil;
124+
125+
if (action.icon != nil) {
126+
UIColor *iconColor = [UIColor blackColor];
127+
128+
if (action.iconColor != nil) {
129+
iconColor = action.iconColor;
130+
}
131+
// Use custom icon from Assets.xcassets
132+
iconImage = [[UIImage imageNamed:action.icon] imageWithTintColor:iconColor];
133+
} else {
134+
// Use system icon from SF Symbols
135+
iconImage = [UIImage systemImageNamed:action.systemIcon];
136+
}
137+
123138
if (action.actions != nil && action.actions.count > 0) {
124139
NSMutableArray<UIMenuElement*> *children = [[NSMutableArray alloc] init];
125140
[action.actions enumerateObjectsUsingBlock:^(ContextMenuAction * _Nonnull childAction, NSUInteger childIdx, BOOL * _Nonnull stop) {
@@ -134,7 +149,7 @@ - (UIMenuElement*) createMenuElementForAction:(ContextMenuAction *)action atInde
134149
(action.inlineChildren ? UIMenuOptionsDisplayInline : 0) |
135150
(action.destructive ? UIMenuOptionsDestructive : 0);
136151
UIMenu *actionMenu = [UIMenu menuWithTitle:action.title
137-
image:[UIImage systemImageNamed:action.systemIcon]
152+
image:iconImage
138153
identifier:nil
139154
options:actionMenuOptions
140155
children:children];
@@ -146,7 +161,7 @@ - (UIMenuElement*) createMenuElementForAction:(ContextMenuAction *)action atInde
146161
menuElement = actionMenu;
147162
} else {
148163
UIAction* actionMenuItem =
149-
[UIAction actionWithTitle:action.title image:[UIImage systemImageNamed:action.systemIcon] identifier:nil handler:^(__kindof UIAction * _Nonnull action) {
164+
[UIAction actionWithTitle:action.title image:iconImage identifier:nil handler:^(__kindof UIAction * _Nonnull action) {
150165
if (self.onPress != nil) {
151166
self->_cancelled = false;
152167
self.onPress(@{

ios/RCTConvert+ContextMenuAction.m

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ + (ContextMenuAction*) ContextMenuAction:(id)json {
1616
action.title = [self NSString:json[@"title"]];
1717
action.subtitle = [self NSString:json[@"subtitle"]];
1818
action.systemIcon = [self NSString:json[@"systemIcon"]];
19+
action.icon = [self NSString:json[@"icon"]];
20+
action.iconColor = json[@"iconColor"] ? [RCTConvert UIColor:json[@"iconColor"]] : nil;
1921
action.destructive = [self BOOL:json[@"destructive"]];
2022
action.selected = [self BOOL:json[@"selected"]];
2123
action.disabled = [self BOOL:json[@"disabled"]];

0 commit comments

Comments
 (0)