Skip to content

Commit d1b9893

Browse files
committed
feat(menu): add selectedIdentifier prop for controlled selection
- Add selectedIdentifier prop to native interfaces - Update Android and iOS implementations to respect the prop - Modify example app to demonstrate controlled usage
1 parent 40db140 commit d1b9893

File tree

6 files changed

+59
-15
lines changed

6 files changed

+59
-15
lines changed

README.md

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,28 @@ const styles = StyleSheet.create({
114114
export default App;
115115
```
116116

117+
### Controlled Selection (Recommended)
118+
119+
Use the `selectedIdentifier` prop to fully control which item is marked as selected. Update it in your `onMenuSelect` handler to keep iOS and Android behavior consistent.
120+
121+
```tsx
122+
const [selectedSort, setSelectedSort] = useState('date');
123+
124+
<MenuView
125+
selectedIdentifier={selectedSort}
126+
menuItems={[
127+
{ identifier: 'date', title: 'Date' },
128+
{ identifier: 'name', title: 'Name' },
129+
{ identifier: 'size', title: 'Size' },
130+
]}
131+
onMenuSelect={({ nativeEvent }) => setSelectedSort(nativeEvent.identifier)}
132+
>
133+
<View style={styles.menuButton}>
134+
<Text>📊 Sort by: {selectedSort}</Text>
135+
</View>
136+
</MenuView>
137+
```
138+
117139
### Custom Styled Trigger
118140

119141
```tsx
@@ -159,6 +181,7 @@ export default App;
159181
| `children` | `ReactNode` | **Yes** | - | The trigger component that opens the menu when tapped |
160182
| `menuItems` | `MenuItem[]` | Yes | `[]` | Array of menu items to display |
161183
| `onMenuSelect` | `(event: MenuSelectEvent) => void` | No | - | Callback fired when a menu item is selected |
184+
| `selectedIdentifier` | `string` | No | - | Controlled selected item identifier; shows a native checkmark for the matching item |
162185
| `checkedColor` | `string` | No | `#007AFF` | Color for checked/selected menu items (Android only) |
163186
| `uncheckedColor` | `string` | No | `#8E8E93` | Color for unchecked/unselected menu items (Android only) |
164187
| `color` | `string` | No | - | Reserved for future use |
@@ -217,8 +240,8 @@ The MenuView component accepts any React Native component as a child, which beco
217240
- **UI:** Native `UIMenu` attached to an invisible `UIButton` overlay
218241
- **Menu Items:** Native `UIAction` elements with system styling
219242
- **Trigger:** Creates transparent button overlay on top of child view to show native menu
220-
- **Checkmarks:** Native system checkmarks with `UIMenuElementStateOn`
221-
- **Selection:** Native iOS menu behavior with smooth animations
243+
- **Checkmarks:** Controlled via `selectedIdentifier` and rendered using `UIMenuElementStateOn`
244+
- **Selection:** Emits `onMenuSelect` without mutating native state; update `selectedIdentifier` in React to reflect changes
222245

223246
**Key Implementation Details:**
224247
- Disables user interaction on child views recursively
@@ -235,6 +258,7 @@ The MenuView component accepts any React Native component as a child, which beco
235258
| Unchecked Color | System default | Fully customizable |
236259
| Animation | Native iOS animation | Slide up animation |
237260
| Scrolling | Native UIMenu scrolling | Custom ScrollView (max 40% screen) |
261+
| Selection State | Controlled via `selectedIdentifier` | use `selectedIdentifier` for cross-platform parity |
238262
| Appearance | iOS system theme | White background with rounded corners |
239263

240264
## Example Project
@@ -272,6 +296,10 @@ yarn android
272296

273297
**Android:** Ensure your child component doesn't have `onPress` or other touch handlers that might interfere. The MenuView intercepts all touch events at the parent level.
274298

299+
### Checkmark not updating on iOS/Android
300+
301+
Pass and update `selectedIdentifier`. iOS does not shift the checkmark automatically—reflect selection in props via your `onMenuSelect` handler.
302+
275303
### Children prop is required
276304

277305
The MenuView component requires a child component to act as the trigger. Always wrap your trigger in the MenuView:

android/src/main/java/com/menu/MenuView.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ class MenuView(context: Context) : FrameLayout(context) {
8787
}
8888
}
8989

90+
fun setSelectedIdentifier(selectedIdentifier: String?) {
91+
this.selectedItemIdentifier = selectedIdentifier
92+
}
93+
9094
fun setMenuItems(menuItems: ReadableArray?) {
9195
val items = mutableListOf<Map<String, String>>()
9296

@@ -288,7 +292,7 @@ class MenuView(context: Context) : FrameLayout(context) {
288292
}
289293

290294
private fun selectMenuItem(identifier: String, title: String) {
291-
selectedItemIdentifier = identifier
295+
// No longer store selectedIdentifier internally - it's controlled by props
292296
sendMenuSelection(identifier, title)
293297
}
294298

android/src/main/java/com/menu/MenuViewManager.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ class MenuViewManager : ViewGroupManager<MenuView>() {
3535
view.setMenuItems(menuItems)
3636
}
3737

38+
@ReactProp(name = "selectedIdentifier")
39+
fun setSelectedIdentifier(view: MenuView, selectedIdentifier: String?) {
40+
view.setSelectedIdentifier(selectedIdentifier)
41+
}
42+
3843
override fun getExportedCustomBubblingEventTypeConstants(): Map<String, Any> {
3944
return mapOf(
4045
"onMenuSelect" to mapOf(

example/src/App.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { useState } from 'react';
1010
import { MenuView } from 'react-native-menus';
1111

1212
const App = () => {
13-
const [selectedTheme, setSelectedTheme] = useState('system');
13+
const [selectedTheme, setSelectedTheme] = useState('dark');
1414
const [selectedSort, setSelectedSort] = useState('date');
1515

1616
const handleMenuSelect = (event: {
@@ -51,6 +51,7 @@ const App = () => {
5151
<MenuView
5252
checkedColor="#007AFF"
5353
uncheckedColor="#8E8E93"
54+
selectedIdentifier={selectedTheme}
5455
menuItems={[
5556
{ identifier: 'light', title: 'Light Mode' },
5657
{ identifier: 'dark', title: 'Dark Mode' },
@@ -78,6 +79,7 @@ const App = () => {
7879
<MenuView
7980
checkedColor="#34C759"
8081
uncheckedColor="#8E8E93"
82+
selectedIdentifier={selectedSort}
8183
menuItems={[
8284
{ identifier: 'date', title: 'Date' },
8385
{ identifier: 'name', title: 'Name' },

ios/MenuView.mm

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ @implementation MenuView {
1919
UIColor *_textColor;
2020
UIColor *_checkedColor;
2121
UIColor *_uncheckedColor;
22-
NSString *_selectedIdentifier;
2322
}
2423

2524
+ (ComponentDescriptorProvider)componentDescriptorProvider
@@ -82,7 +81,7 @@ - (void)setupChildViewAsMenuTrigger:(UIView *)childView
8281
if ([childView isKindOfClass:[UIButton class]]) {
8382
_menuButton = (UIButton *)childView;
8483
_menuButton.showsMenuAsPrimaryAction = YES;
85-
[self updateMenuItems:_menuItems];
84+
[self updateMenuItems:_menuItems selectedIdentifier:nil];
8685
} else {
8786
// For non-button children, create an invisible button overlay to show the menu
8887
[self disableUserInteractionRecursively:childView];
@@ -103,7 +102,7 @@ - (void)setupChildViewAsMenuTrigger:(UIView *)childView
103102
[_menuButton.bottomAnchor constraintEqualToAnchor:self.bottomAnchor]
104103
]];
105104

106-
[self updateMenuItems:_menuItems];
105+
[self updateMenuItems:_menuItems selectedIdentifier:nil];
107106
}
108107
}
109108

@@ -156,6 +155,13 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
156155
}
157156
}
158157

158+
// Detect selectedIdentifier change
159+
bool selectedIdentifierChanged = (oldViewProps.selectedIdentifier != newViewProps.selectedIdentifier);
160+
NSString *currentSelectedIdentifier = nil;
161+
if (!newViewProps.selectedIdentifier.empty()) {
162+
currentSelectedIdentifier = [[NSString alloc] initWithUTF8String:newViewProps.selectedIdentifier.c_str()];
163+
}
164+
159165
if (menuItemsChanged) {
160166
NSMutableArray *items = [[NSMutableArray alloc] init];
161167
for (const auto &item : newViewProps.menuItems) {
@@ -164,13 +170,15 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
164170
[items addObject:@{@"identifier": identifier, @"title": title}];
165171
}
166172
_menuItems = [items copy];
167-
[self updateMenuItems:_menuItems];
173+
[self updateMenuItems:_menuItems selectedIdentifier:currentSelectedIdentifier];
174+
} else if (selectedIdentifierChanged) {
175+
[self updateMenuItems:_menuItems selectedIdentifier:currentSelectedIdentifier];
168176
}
169177

170178
[super updateProps:props oldProps:oldProps];
171179
}
172180

173-
- (void)updateMenuItems:(NSArray<NSDictionary *> *)menuItems
181+
- (void)updateMenuItems:(NSArray<NSDictionary *> *)menuItems selectedIdentifier:(NSString *)selectedIdentifier
174182
{
175183
if (!_menuButton) {
176184
// Menu button not set yet, will be updated when child view is added
@@ -195,8 +203,8 @@ - (void)updateMenuItems:(NSArray<NSDictionary *> *)menuItems
195203
[self selectMenuItem:identifier title:title];
196204
}];
197205

198-
// Set state based on current selection
199-
if ([identifier isEqualToString:_selectedIdentifier]) {
206+
// Set state based on current selection (controlled via props)
207+
if (selectedIdentifier != nil && [identifier isEqualToString:selectedIdentifier]) {
200208
action.state = UIMenuElementStateOn;
201209
}
202210

@@ -209,10 +217,6 @@ - (void)updateMenuItems:(NSArray<NSDictionary *> *)menuItems
209217

210218
- (void)selectMenuItem:(NSString *)identifier title:(NSString *)title
211219
{
212-
_selectedIdentifier = identifier;
213-
if (_menuButton) {
214-
[self updateMenuItems:_menuItems]; // Refresh to update checkmarks
215-
}
216220
[self sendMenuSelection:identifier title:title];
217221
}
218222

src/MenuViewNativeComponent.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export interface NativeProps extends ViewProps {
1717
checkedColor?: string;
1818
uncheckedColor?: string;
1919
menuItems?: ReadonlyArray<MenuItem>;
20+
selectedIdentifier?: string;
2021
onMenuSelect?: BubblingEventHandler<MenuSelectEvent>;
2122
}
2223

0 commit comments

Comments
 (0)