Skip to content

Commit

Permalink
feat: Add onOpenMenu and onCloseMenu event handlers (react-native-men…
Browse files Browse the repository at this point in the history
…u#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]>
  • Loading branch information
malkailany and mohammed authored Dec 27, 2024
1 parent 0a38464 commit d7dcacd
Show file tree
Hide file tree
Showing 17 changed files with 324 additions and 74 deletions.
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,40 @@ It will contain id of the given action.
|-------------------------|----------|
| ({nativeEvent}) => void | No |
### Events
#### `onCloseMenu`
Callback function that will be called when the menu is dismissed. This event fires at the start of the dismissal, before any animations complete.
| Type | Required |
|------------|----------|
| () => void | No |
#### `onOpenMenu`
Callback function that will be called when the menu is opened. This event fires right before the menu is displayed.
| Type | Required |
|------------|----------|
| () => void | No |
Example usage:
```jsx
<MenuView
onOpenMenu={() => {
console.log('Menu was opened');
}}
onCloseMenu={() => {
console.log('Menu was closed');
}}
// ... other props
>
<View>
<Text>Open Menu</Text>
</View>
</MenuView>
```
## Testing with Jest
In some cases, you might want to mock the package to test your components. You can do this by using the `jest.mock` function.
Expand Down
13 changes: 13 additions & 0 deletions android/src/main/java/com/reactnativemenu/MenuOnCloseEvent.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.reactnativemenu

import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event

class MenuOnCloseEvent(surfaceId: Int, viewId: Int, private val targetId: Int) : Event<MenuOnCloseEvent>(surfaceId, viewId) {
override fun getEventName() = "onCloseMenu"

override fun getEventData(): WritableMap? {
return Arguments.createMap()
}
}
13 changes: 13 additions & 0 deletions android/src/main/java/com/reactnativemenu/MenuOnOpenEvent.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.reactnativemenu

import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event

class MenuOnOpenEvent(surfaceId: Int, viewId: Int, private val targetId: Int) : Event<MenuOnOpenEvent>(surfaceId, viewId) {
override fun getEventName() = "onOpenMenu"

override fun getEventData(): WritableMap? {
return Arguments.createMap()
}
}
6 changes: 6 additions & 0 deletions android/src/main/java/com/reactnativemenu/MenuView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -261,8 +261,14 @@ class MenuView(private val mContext: ReactContext) : ReactViewGroup(mContext) {
}
mPopupMenu.setOnDismissListener {
mIsMenuDisplayed = false
val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(mContext, id)
val surfaceId: Int = UIManagerHelper.getSurfaceId(this)
dispatcher?.dispatchEvent(MenuOnCloseEvent(surfaceId, id, id))
}
mIsMenuDisplayed = true
val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(mContext, id)
val surfaceId: Int = UIManagerHelper.getSurfaceId(this)
dispatcher?.dispatchEvent(MenuOnOpenEvent(surfaceId, id, id))
mPopupMenu.show()
}
}
Expand Down
101 changes: 78 additions & 23 deletions android/src/main/java/com/reactnativemenu/MenuViewManagerBase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import com.facebook.react.views.view.ReactDrawableHelper
import com.facebook.react.views.view.ReactViewGroup
import com.facebook.yoga.YogaConstants

abstract class MenuViewManagerBase: ReactClippingViewManager<MenuView>() {
abstract class MenuViewManagerBase : ReactClippingViewManager<MenuView>() {
override fun getName() = "MenuView"

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

override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> {
return MapBuilder.of(
"onPressAction",
MapBuilder.of("registrationName", "onPressAction")
"onPressAction",
MapBuilder.of("registrationName", "onPressAction"),
"onCloseMenu",
MapBuilder.of("registrationName", "onCloseMenu"),
"onOpenMenu",
MapBuilder.of("registrationName", "onOpenMenu")
)
}

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

@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])
@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]
)
fun setBorderRadius(view: ReactViewGroup, index: Int, borderRadius: Float) {
var borderRadius = borderRadius
if (!YogaConstants.isUndefined(borderRadius) && borderRadius < 0) {
Expand All @@ -109,33 +125,60 @@ abstract class MenuViewManagerBase: ReactClippingViewManager<MenuView>() {
// We should keep using setters as `Val cannot be reassigned`
view.setHitSlopRect(null)
} else {
view.setHitSlopRect(Rect(
if (hitSlop.hasKey("left")) PixelUtil.toPixelFromDIP(hitSlop.getDouble("left")).toInt() else 0,
if (hitSlop.hasKey("top")) PixelUtil.toPixelFromDIP(hitSlop.getDouble("top")).toInt() else 0,
if (hitSlop.hasKey("right")) PixelUtil.toPixelFromDIP(hitSlop.getDouble("right")).toInt() else 0,
if (hitSlop.hasKey("bottom")) PixelUtil.toPixelFromDIP(hitSlop.getDouble("bottom")).toInt() else 0))
view.setHitSlopRect(
Rect(
if (hitSlop.hasKey("left"))
PixelUtil.toPixelFromDIP(hitSlop.getDouble("left")).toInt()
else 0,
if (hitSlop.hasKey("top"))
PixelUtil.toPixelFromDIP(hitSlop.getDouble("top")).toInt()
else 0,
if (hitSlop.hasKey("right"))
PixelUtil.toPixelFromDIP(hitSlop.getDouble("right")).toInt()
else 0,
if (hitSlop.hasKey("bottom"))
PixelUtil.toPixelFromDIP(hitSlop.getDouble("bottom")).toInt()
else 0
)
)
}
}

@ReactProp(name = "nativeBackgroundAndroid")
fun setNativeBackground(view: ReactViewGroup, @Nullable bg: ReadableMap?) {
view.setTranslucentBackgroundDrawable(
if (bg == null) null else ReactDrawableHelper.createDrawableFromJSDescription(view.context, bg))
if (bg == null) null
else ReactDrawableHelper.createDrawableFromJSDescription(view.context, bg)
)
}

@TargetApi(Build.VERSION_CODES.M)
@ReactProp(name = "nativeForegroundAndroid")
fun setNativeForeground(view: ReactViewGroup, @Nullable fg: ReadableMap?) {
view.foreground = if (fg == null) null else ReactDrawableHelper.createDrawableFromJSDescription(view.context, fg)
view.foreground =
if (fg == null) null
else ReactDrawableHelper.createDrawableFromJSDescription(view.context, fg)
}

@ReactProp(name = ViewProps.NEEDS_OFFSCREEN_ALPHA_COMPOSITING)
fun setNeedsOffscreenAlphaCompositing(
view: ReactViewGroup, needsOffscreenAlphaCompositing: Boolean) {
view: ReactViewGroup,
needsOffscreenAlphaCompositing: Boolean
) {
view.setNeedsOffscreenAlphaCompositing(needsOffscreenAlphaCompositing)
}

@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])
@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]
)
fun setBorderWidth(view: ReactViewGroup, index: Int, width: Float) {
var width = width
if (!YogaConstants.isUndefined(width) && width < 0) {
Expand All @@ -147,7 +190,18 @@ abstract class MenuViewManagerBase: ReactClippingViewManager<MenuView>() {
view.setBorderWidth(SPACING_TYPES[index], width)
}

@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")
@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"
)
abstract fun setBorderColor(view: ReactViewGroup, index: Int, color: Int?)

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

companion object {
val COMMAND_SHOW = 1
val SPACING_TYPES = arrayOf(
Spacing.ALL,
Spacing.LEFT,
Spacing.RIGHT,
Spacing.TOP,
Spacing.BOTTOM,
Spacing.START,
Spacing.END
)
val SPACING_TYPES =
arrayOf(
Spacing.ALL,
Spacing.LEFT,
Spacing.RIGHT,
Spacing.TOP,
Spacing.BOTTOM,
Spacing.START,
Spacing.END
)
}
}
8 changes: 8 additions & 0 deletions ios/MenuViewManager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ - (UIView *)view
* onPressAction: callback to be called once user selects an action
*/
RCT_EXPORT_VIEW_PROPERTY(onPressAction, RCTDirectEventBlock);
/**
* onCloseMenu: callback to be called when the menu is closed
*/
RCT_EXPORT_VIEW_PROPERTY(onCloseMenu, RCTDirectEventBlock);
/**
* onOpenMenu: callback to be called when the menu is opened
*/
RCT_EXPORT_VIEW_PROPERTY(onOpenMenu, RCTDirectEventBlock);
/**
* shouldOpenOnLongPress: determines whether menu should be opened after long press or normal press
*/
Expand Down
16 changes: 14 additions & 2 deletions ios/NewArch/FabricActionSheetView.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@

@objc(FabricActionSheetView)
public class FabricActionSheetView: ActionSheetView, FabricViewImplementationProtocol {
public var onPressAction: ((String) -> Void)?

public var onCloseMenu: (() -> Void)?
public var onOpenMenu: (() -> Void)?

@objc override func sendButtonAction(_ action: String) {
if let onPress = onPressAction {
onPress(action)
}
}

@objc override func sendMenuClose() {
if let onCloseMenu = onCloseMenu {
onCloseMenu()
}
}
@objc override func sendMenuOpen() {
if let onOpenMenu = onOpenMenu {
onOpenMenu()
}
}
}
16 changes: 15 additions & 1 deletion ios/NewArch/FabricMenuViewImplementation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,25 @@ import UIKit
@objc(FabricMenuViewImplementation)
public class FabricMenuViewImplementation: MenuViewImplementation, FabricViewImplementationProtocol {
public var onPressAction: ((String) -> Void)?

public var onCloseMenu: (() -> Void)?
public var onOpenMenu: (() -> Void)?

@objc override func sendButtonAction(_ action: UIAction) {
if let onPress = onPressAction {
onPress(action.identifier.rawValue)
}
}

@objc override func sendMenuClose() {
if let onCloseMenu = onCloseMenu {
onCloseMenu()
}
}

@objc override func sendMenuOpen() {
if let onOpenMenu = onOpenMenu {
onOpenMenu()
}
}

}
2 changes: 2 additions & 0 deletions ios/NewArch/FabricViewImplementationProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ import Foundation
var shouldOpenOnLongPress: Bool { get set }
@objc optional var hitSlop: UIEdgeInsets { get set }
var onPressAction: ((String) -> Void)? { get set }
var onCloseMenu: (() -> Void)? { get set }
var onOpenMenu: (() -> Void)? { get set }
}
19 changes: 19 additions & 0 deletions ios/NewArch/MenuView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ - (instancetype)initWithFrame:(CGRect)frame
_view.onPressAction = ^(NSString *eventString) {
[self onPressAction:eventString];
};
_view.onCloseMenu = ^{
[self onCloseMenu];
};
self.contentView = _view;
}

Expand Down Expand Up @@ -68,6 +71,22 @@ - (void)onPressAction:(NSString * _Nonnull)eventString {
}
}

- (void)onCloseMenu {
// If screen is already unmounted then there will be no event emitter
const auto eventEmitter = [self getEventEmitter];
if (eventEmitter != nullptr) {
eventEmitter->onCloseMenu({});
}
}

- (void)onOpenMenu {
// If screen is already unmounted then there will be no event emitter
const auto eventEmitter = [self getEventEmitter];
if (eventEmitter != nullptr) {
eventEmitter->onOpenMenu({});
}
}

/**
Responsible for iterating through the C++ vector<struct> and convert each struct element to NSDictionary, then return it all in an NSArray
*/
Expand Down
19 changes: 17 additions & 2 deletions ios/OldArch/LegacyActionSheetView.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@

@objc(LegacyActionSheetView)
public class LegacyActionSheetView: ActionSheetView {
@objc var onPressAction: RCTDirectEventBlock?

@objc var onCloseMenu: RCTDirectEventBlock?
@objc var onOpenMenu: RCTDirectEventBlock?



@objc override func sendButtonAction(_ action: String) {
if let onPress = onPressAction {
onPress(["event":action])
}
}

@objc override func sendMenuClose() {
if let onCloseMenu = onCloseMenu {
onCloseMenu([:])
}
}

@objc override func sendMenuOpen() {
if let onOpenMenu = onOpenMenu {
onOpenMenu([:])
}
}
}
Loading

0 comments on commit d7dcacd

Please sign in to comment.