Skip to content

Commit d7dcacd

Browse files
malkailanymohammed
and
mohammed
authored
feat: Add onOpenMenu and onCloseMenu event handlers (#998)
* revert: back to original * feat: close menu support * feat: close menu support * feat: close menu support * feat: close menu support * feat: close menu support * chore: removed package lock file * feat(events): add menu close detection Add onMenuClose event that fires when menu is dismissed. - Implement for both iOS and Android platforms - Add event at start of dismissal for better responsiveness - Support both old and new React Native architectures - Add tests and update documentation The event fires when: - User taps outside the menu - User selects a menu item * feat(events): add onOpenMenu event and rename onMenuClose * refactor(menu): simplify menu event handlers by removing native event parameters * fix(types): update onCloseMenu and onOpenMenu event handlers to use undefined event parameter * fix(types): update onCloseMenu and onOpenMenu event handlers to accept string event parameters * refactor(menu): rename onMenuOpen to onOpenMenu across iOS implementations * feat(menu): implement onOpenMenu event and update event handling in iOS and Android * refactor(menu): streamline event handling for onCloseMenu and onOpenMenu --------- Co-authored-by: mohammed <[email protected]>
1 parent 0a38464 commit d7dcacd

17 files changed

+324
-74
lines changed

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,40 @@ It will contain id of the given action.
291291
|-------------------------|----------|
292292
| ({nativeEvent}) => void | No |
293293
294+
### Events
295+
296+
#### `onCloseMenu`
297+
298+
Callback function that will be called when the menu is dismissed. This event fires at the start of the dismissal, before any animations complete.
299+
300+
| Type | Required |
301+
|------------|----------|
302+
| () => void | No |
303+
304+
#### `onOpenMenu`
305+
306+
Callback function that will be called when the menu is opened. This event fires right before the menu is displayed.
307+
308+
| Type | Required |
309+
|------------|----------|
310+
| () => void | No |
311+
312+
Example usage:
313+
```jsx
314+
<MenuView
315+
onOpenMenu={() => {
316+
console.log('Menu was opened');
317+
}}
318+
onCloseMenu={() => {
319+
console.log('Menu was closed');
320+
}}
321+
// ... other props
322+
>
323+
<View>
324+
<Text>Open Menu</Text>
325+
</View>
326+
</MenuView>
327+
```
294328
## Testing with Jest
295329
296330
In some cases, you might want to mock the package to test your components. You can do this by using the `jest.mock` function.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.reactnativemenu
2+
3+
import com.facebook.react.bridge.Arguments
4+
import com.facebook.react.bridge.WritableMap
5+
import com.facebook.react.uimanager.events.Event
6+
7+
class MenuOnCloseEvent(surfaceId: Int, viewId: Int, private val targetId: Int) : Event<MenuOnCloseEvent>(surfaceId, viewId) {
8+
override fun getEventName() = "onCloseMenu"
9+
10+
override fun getEventData(): WritableMap? {
11+
return Arguments.createMap()
12+
}
13+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.reactnativemenu
2+
3+
import com.facebook.react.bridge.Arguments
4+
import com.facebook.react.bridge.WritableMap
5+
import com.facebook.react.uimanager.events.Event
6+
7+
class MenuOnOpenEvent(surfaceId: Int, viewId: Int, private val targetId: Int) : Event<MenuOnOpenEvent>(surfaceId, viewId) {
8+
override fun getEventName() = "onOpenMenu"
9+
10+
override fun getEventData(): WritableMap? {
11+
return Arguments.createMap()
12+
}
13+
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,8 +261,14 @@ class MenuView(private val mContext: ReactContext) : ReactViewGroup(mContext) {
261261
}
262262
mPopupMenu.setOnDismissListener {
263263
mIsMenuDisplayed = false
264+
val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(mContext, id)
265+
val surfaceId: Int = UIManagerHelper.getSurfaceId(this)
266+
dispatcher?.dispatchEvent(MenuOnCloseEvent(surfaceId, id, id))
264267
}
265268
mIsMenuDisplayed = true
269+
val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(mContext, id)
270+
val surfaceId: Int = UIManagerHelper.getSurfaceId(this)
271+
dispatcher?.dispatchEvent(MenuOnOpenEvent(surfaceId, id, id))
266272
mPopupMenu.show()
267273
}
268274
}

android/src/main/java/com/reactnativemenu/MenuViewManagerBase.kt

Lines changed: 78 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import com.facebook.react.views.view.ReactDrawableHelper
1717
import com.facebook.react.views.view.ReactViewGroup
1818
import com.facebook.yoga.YogaConstants
1919

20-
abstract class MenuViewManagerBase: ReactClippingViewManager<MenuView>() {
20+
abstract class MenuViewManagerBase : ReactClippingViewManager<MenuView>() {
2121
override fun getName() = "MenuView"
2222

2323
@ReactProp(name = "actions")
@@ -37,8 +37,12 @@ abstract class MenuViewManagerBase: ReactClippingViewManager<MenuView>() {
3737

3838
override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> {
3939
return MapBuilder.of(
40-
"onPressAction",
41-
MapBuilder.of("registrationName", "onPressAction")
40+
"onPressAction",
41+
MapBuilder.of("registrationName", "onPressAction"),
42+
"onCloseMenu",
43+
MapBuilder.of("registrationName", "onCloseMenu"),
44+
"onOpenMenu",
45+
MapBuilder.of("registrationName", "onOpenMenu")
4246
)
4347
}
4448

@@ -82,7 +86,19 @@ abstract class MenuViewManagerBase: ReactClippingViewManager<MenuView>() {
8286
view.nextFocusUpId = viewId
8387
}
8488

85-
@ReactPropGroup(names = [ViewProps.BORDER_RADIUS, ViewProps.BORDER_TOP_LEFT_RADIUS, ViewProps.BORDER_TOP_RIGHT_RADIUS, ViewProps.BORDER_BOTTOM_RIGHT_RADIUS, ViewProps.BORDER_BOTTOM_LEFT_RADIUS, ViewProps.BORDER_TOP_START_RADIUS, ViewProps.BORDER_TOP_END_RADIUS, ViewProps.BORDER_BOTTOM_START_RADIUS, ViewProps.BORDER_BOTTOM_END_RADIUS])
89+
@ReactPropGroup(
90+
names =
91+
[
92+
ViewProps.BORDER_RADIUS,
93+
ViewProps.BORDER_TOP_LEFT_RADIUS,
94+
ViewProps.BORDER_TOP_RIGHT_RADIUS,
95+
ViewProps.BORDER_BOTTOM_RIGHT_RADIUS,
96+
ViewProps.BORDER_BOTTOM_LEFT_RADIUS,
97+
ViewProps.BORDER_TOP_START_RADIUS,
98+
ViewProps.BORDER_TOP_END_RADIUS,
99+
ViewProps.BORDER_BOTTOM_START_RADIUS,
100+
ViewProps.BORDER_BOTTOM_END_RADIUS]
101+
)
86102
fun setBorderRadius(view: ReactViewGroup, index: Int, borderRadius: Float) {
87103
var borderRadius = borderRadius
88104
if (!YogaConstants.isUndefined(borderRadius) && borderRadius < 0) {
@@ -109,33 +125,60 @@ abstract class MenuViewManagerBase: ReactClippingViewManager<MenuView>() {
109125
// We should keep using setters as `Val cannot be reassigned`
110126
view.setHitSlopRect(null)
111127
} else {
112-
view.setHitSlopRect(Rect(
113-
if (hitSlop.hasKey("left")) PixelUtil.toPixelFromDIP(hitSlop.getDouble("left")).toInt() else 0,
114-
if (hitSlop.hasKey("top")) PixelUtil.toPixelFromDIP(hitSlop.getDouble("top")).toInt() else 0,
115-
if (hitSlop.hasKey("right")) PixelUtil.toPixelFromDIP(hitSlop.getDouble("right")).toInt() else 0,
116-
if (hitSlop.hasKey("bottom")) PixelUtil.toPixelFromDIP(hitSlop.getDouble("bottom")).toInt() else 0))
128+
view.setHitSlopRect(
129+
Rect(
130+
if (hitSlop.hasKey("left"))
131+
PixelUtil.toPixelFromDIP(hitSlop.getDouble("left")).toInt()
132+
else 0,
133+
if (hitSlop.hasKey("top"))
134+
PixelUtil.toPixelFromDIP(hitSlop.getDouble("top")).toInt()
135+
else 0,
136+
if (hitSlop.hasKey("right"))
137+
PixelUtil.toPixelFromDIP(hitSlop.getDouble("right")).toInt()
138+
else 0,
139+
if (hitSlop.hasKey("bottom"))
140+
PixelUtil.toPixelFromDIP(hitSlop.getDouble("bottom")).toInt()
141+
else 0
142+
)
143+
)
117144
}
118145
}
119146

120147
@ReactProp(name = "nativeBackgroundAndroid")
121148
fun setNativeBackground(view: ReactViewGroup, @Nullable bg: ReadableMap?) {
122149
view.setTranslucentBackgroundDrawable(
123-
if (bg == null) null else ReactDrawableHelper.createDrawableFromJSDescription(view.context, bg))
150+
if (bg == null) null
151+
else ReactDrawableHelper.createDrawableFromJSDescription(view.context, bg)
152+
)
124153
}
125154

126155
@TargetApi(Build.VERSION_CODES.M)
127156
@ReactProp(name = "nativeForegroundAndroid")
128157
fun setNativeForeground(view: ReactViewGroup, @Nullable fg: ReadableMap?) {
129-
view.foreground = if (fg == null) null else ReactDrawableHelper.createDrawableFromJSDescription(view.context, fg)
158+
view.foreground =
159+
if (fg == null) null
160+
else ReactDrawableHelper.createDrawableFromJSDescription(view.context, fg)
130161
}
131162

132163
@ReactProp(name = ViewProps.NEEDS_OFFSCREEN_ALPHA_COMPOSITING)
133164
fun setNeedsOffscreenAlphaCompositing(
134-
view: ReactViewGroup, needsOffscreenAlphaCompositing: Boolean) {
165+
view: ReactViewGroup,
166+
needsOffscreenAlphaCompositing: Boolean
167+
) {
135168
view.setNeedsOffscreenAlphaCompositing(needsOffscreenAlphaCompositing)
136169
}
137170

138-
@ReactPropGroup(names = [ViewProps.BORDER_WIDTH, ViewProps.BORDER_LEFT_WIDTH, ViewProps.BORDER_RIGHT_WIDTH, ViewProps.BORDER_TOP_WIDTH, ViewProps.BORDER_BOTTOM_WIDTH, ViewProps.BORDER_START_WIDTH, ViewProps.BORDER_END_WIDTH])
171+
@ReactPropGroup(
172+
names =
173+
[
174+
ViewProps.BORDER_WIDTH,
175+
ViewProps.BORDER_LEFT_WIDTH,
176+
ViewProps.BORDER_RIGHT_WIDTH,
177+
ViewProps.BORDER_TOP_WIDTH,
178+
ViewProps.BORDER_BOTTOM_WIDTH,
179+
ViewProps.BORDER_START_WIDTH,
180+
ViewProps.BORDER_END_WIDTH]
181+
)
139182
fun setBorderWidth(view: ReactViewGroup, index: Int, width: Float) {
140183
var width = width
141184
if (!YogaConstants.isUndefined(width) && width < 0) {
@@ -147,7 +190,18 @@ abstract class MenuViewManagerBase: ReactClippingViewManager<MenuView>() {
147190
view.setBorderWidth(SPACING_TYPES[index], width)
148191
}
149192

150-
@ReactPropGroup(names = [ViewProps.BORDER_COLOR, ViewProps.BORDER_LEFT_COLOR, ViewProps.BORDER_RIGHT_COLOR, ViewProps.BORDER_TOP_COLOR, ViewProps.BORDER_BOTTOM_COLOR, ViewProps.BORDER_START_COLOR, ViewProps.BORDER_END_COLOR], customType = "Color")
193+
@ReactPropGroup(
194+
names =
195+
[
196+
ViewProps.BORDER_COLOR,
197+
ViewProps.BORDER_LEFT_COLOR,
198+
ViewProps.BORDER_RIGHT_COLOR,
199+
ViewProps.BORDER_TOP_COLOR,
200+
ViewProps.BORDER_BOTTOM_COLOR,
201+
ViewProps.BORDER_START_COLOR,
202+
ViewProps.BORDER_END_COLOR],
203+
customType = "Color"
204+
)
151205
abstract fun setBorderColor(view: ReactViewGroup, index: Int, color: Int?)
152206

153207
@ReactProp(name = ViewProps.OVERFLOW)
@@ -181,14 +235,15 @@ abstract class MenuViewManagerBase: ReactClippingViewManager<MenuView>() {
181235

182236
companion object {
183237
val COMMAND_SHOW = 1
184-
val SPACING_TYPES = arrayOf(
185-
Spacing.ALL,
186-
Spacing.LEFT,
187-
Spacing.RIGHT,
188-
Spacing.TOP,
189-
Spacing.BOTTOM,
190-
Spacing.START,
191-
Spacing.END
192-
)
238+
val SPACING_TYPES =
239+
arrayOf(
240+
Spacing.ALL,
241+
Spacing.LEFT,
242+
Spacing.RIGHT,
243+
Spacing.TOP,
244+
Spacing.BOTTOM,
245+
Spacing.START,
246+
Spacing.END
247+
)
193248
}
194249
}

ios/MenuViewManager.mm

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ - (UIView *)view
5757
* onPressAction: callback to be called once user selects an action
5858
*/
5959
RCT_EXPORT_VIEW_PROPERTY(onPressAction, RCTDirectEventBlock);
60+
/**
61+
* onCloseMenu: callback to be called when the menu is closed
62+
*/
63+
RCT_EXPORT_VIEW_PROPERTY(onCloseMenu, RCTDirectEventBlock);
64+
/**
65+
* onOpenMenu: callback to be called when the menu is opened
66+
*/
67+
RCT_EXPORT_VIEW_PROPERTY(onOpenMenu, RCTDirectEventBlock);
6068
/**
6169
* shouldOpenOnLongPress: determines whether menu should be opened after long press or normal press
6270
*/
Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
1-
21
@objc(FabricActionSheetView)
32
public class FabricActionSheetView: ActionSheetView, FabricViewImplementationProtocol {
43
public var onPressAction: ((String) -> Void)?
5-
4+
public var onCloseMenu: (() -> Void)?
5+
public var onOpenMenu: (() -> Void)?
6+
67
@objc override func sendButtonAction(_ action: String) {
78
if let onPress = onPressAction {
89
onPress(action)
910
}
1011
}
12+
13+
@objc override func sendMenuClose() {
14+
if let onCloseMenu = onCloseMenu {
15+
onCloseMenu()
16+
}
17+
}
18+
@objc override func sendMenuOpen() {
19+
if let onOpenMenu = onOpenMenu {
20+
onOpenMenu()
21+
}
22+
}
1123
}

ios/NewArch/FabricMenuViewImplementation.swift

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,25 @@ import UIKit
1010
@objc(FabricMenuViewImplementation)
1111
public class FabricMenuViewImplementation: MenuViewImplementation, FabricViewImplementationProtocol {
1212
public var onPressAction: ((String) -> Void)?
13-
13+
public var onCloseMenu: (() -> Void)?
14+
public var onOpenMenu: (() -> Void)?
15+
1416
@objc override func sendButtonAction(_ action: UIAction) {
1517
if let onPress = onPressAction {
1618
onPress(action.identifier.rawValue)
1719
}
1820
}
1921

22+
@objc override func sendMenuClose() {
23+
if let onCloseMenu = onCloseMenu {
24+
onCloseMenu()
25+
}
26+
}
27+
28+
@objc override func sendMenuOpen() {
29+
if let onOpenMenu = onOpenMenu {
30+
onOpenMenu()
31+
}
32+
}
33+
2034
}

ios/NewArch/FabricViewImplementationProtocol.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@ import Foundation
66
var shouldOpenOnLongPress: Bool { get set }
77
@objc optional var hitSlop: UIEdgeInsets { get set }
88
var onPressAction: ((String) -> Void)? { get set }
9+
var onCloseMenu: (() -> Void)? { get set }
10+
var onOpenMenu: (() -> Void)? { get set }
911
}

ios/NewArch/MenuView.mm

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ - (instancetype)initWithFrame:(CGRect)frame
4141
_view.onPressAction = ^(NSString *eventString) {
4242
[self onPressAction:eventString];
4343
};
44+
_view.onCloseMenu = ^{
45+
[self onCloseMenu];
46+
};
4447
self.contentView = _view;
4548
}
4649

@@ -68,6 +71,22 @@ - (void)onPressAction:(NSString * _Nonnull)eventString {
6871
}
6972
}
7073

74+
- (void)onCloseMenu {
75+
// If screen is already unmounted then there will be no event emitter
76+
const auto eventEmitter = [self getEventEmitter];
77+
if (eventEmitter != nullptr) {
78+
eventEmitter->onCloseMenu({});
79+
}
80+
}
81+
82+
- (void)onOpenMenu {
83+
// If screen is already unmounted then there will be no event emitter
84+
const auto eventEmitter = [self getEventEmitter];
85+
if (eventEmitter != nullptr) {
86+
eventEmitter->onOpenMenu({});
87+
}
88+
}
89+
7190
/**
7291
Responsible for iterating through the C++ vector<struct> and convert each struct element to NSDictionary, then return it all in an NSArray
7392
*/
Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,26 @@
1-
21
@objc(LegacyActionSheetView)
32
public class LegacyActionSheetView: ActionSheetView {
43
@objc var onPressAction: RCTDirectEventBlock?
5-
4+
@objc var onCloseMenu: RCTDirectEventBlock?
5+
@objc var onOpenMenu: RCTDirectEventBlock?
6+
7+
8+
69
@objc override func sendButtonAction(_ action: String) {
710
if let onPress = onPressAction {
811
onPress(["event":action])
912
}
1013
}
14+
15+
@objc override func sendMenuClose() {
16+
if let onCloseMenu = onCloseMenu {
17+
onCloseMenu([:])
18+
}
19+
}
20+
21+
@objc override func sendMenuOpen() {
22+
if let onOpenMenu = onOpenMenu {
23+
onOpenMenu([:])
24+
}
25+
}
1126
}

0 commit comments

Comments
 (0)