diff --git a/README.md b/README.md index 2892d1eb..1c26238a 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,14 @@ Boolean determining if menu should open after long press or on normal press |---------|----------| | boolean | No | +### `onPress` + +Callback fired when the anchor is tapped (a normal press, not a long press). Only meaningful together with [`shouldOpenOnLongPress`](#shouldopenonlongpress): the menu opens on long press while a normal tap invokes this — letting the anchor be both tappable and long-pressable (e.g. tap a row to open it, long-press for a context menu). + +| Type | Required | +|------------|----------| +| () => void | No | + ### `actions` Actions to be displayed in the menu. diff --git a/android/src/main/java/com/reactnativemenu/MenuView.kt b/android/src/main/java/com/reactnativemenu/MenuView.kt index 16fb9946..9ef12dd6 100644 --- a/android/src/main/java/com/reactnativemenu/MenuView.kt +++ b/android/src/main/java/com/reactnativemenu/MenuView.kt @@ -41,6 +41,14 @@ class MenuView(private val mContext: ReactContext) : ReactViewGroup(mContext) { override fun onSingleTapUp(e: MotionEvent): Boolean { if (!mIsOnLongPress) { prepareMenu() + } else { + // In long-press mode a normal tap acts as a press, emitted through + // onPressAction with a sentinel id. + val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(mContext, id) + val surfaceId: Int = UIManagerHelper.getSurfaceId(this) + dispatcher?.dispatchEvent( + MenuOnPressActionEvent(surfaceId, id, "rnmenu:onPress", id) + ) } return true } diff --git a/ios/Shared/ActionSheetView.swift b/ios/Shared/ActionSheetView.swift index 352d8fa6..3100b895 100644 --- a/ios/Shared/ActionSheetView.swift +++ b/ios/Shared/ActionSheetView.swift @@ -89,6 +89,10 @@ public class ActionSheetView: UIView { @objc func handleTap(_ sender:UITapGestureRecognizer) { if shouldOpenOnLongPress { + // Tap acts as a press when the sheet opens on long press. + if sender.state == .ended { + self.sendButtonAction("rnmenu:onPress") + } return } if sender.state == .ended { diff --git a/ios/Shared/MenuViewImplementation.swift b/ios/Shared/MenuViewImplementation.swift index 5c4e0da4..307495bb 100644 --- a/ios/Shared/MenuViewImplementation.swift +++ b/ios/Shared/MenuViewImplementation.swift @@ -56,8 +56,21 @@ public class MenuViewImplementation: UIButton { super.init(frame: frame) let interaction = UIContextMenuInteraction(delegate: self) self.addInteraction(interaction) + // In long-press mode the menu opens via the context-menu interaction, + // leaving a normal tap free. The UIButton fires touchUpInside on tap; + // forward it through the action callback with a sentinel id. + self.addTarget(self, action: #selector(handlePress), for: .touchUpInside) self.setup() } + + // Emit a press only when the menu opens on long press (primary action + // disabled); in primary-action mode the tap already opens the menu. + @objc func handlePress() { + if !self.showsMenuAsPrimaryAction { + let action = UIAction(title: "", identifier: UIAction.Identifier("rnmenu:onPress")) { _ in } + self.sendButtonAction(action) + } + } public override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? { sendMenuOpen() diff --git a/src/index.tsx b/src/index.tsx index a5362981..475c5713 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -22,8 +22,14 @@ function processAction(action: MenuAction): ProcessedMenuAction { const defaultHitslop = { top: 0, left: 0, bottom: 0, right: 0 }; +// Sentinel action id the native side emits through onPressAction on a plain +// tap of the anchor (only when shouldOpenOnLongPress is set). The JS layer +// translates it into the public `onPress` callback so an anchor can be both +// tapped and long-pressed. +const ON_PRESS_EVENT = "rnmenu:onPress"; + const MenuView = forwardRef( - ({ actions, hitSlop = defaultHitslop, ...props }, ref) => { + ({ actions, hitSlop = defaultHitslop, onPress, onPressAction, ...props }, ref) => { const processedActions = actions.map((action) => processAction(action), ); @@ -37,6 +43,13 @@ const MenuView = forwardRef( hitSlop={hitSlop} actions={processedActions} actionsHash={hash} + onPressAction={(event) => { + if (event.nativeEvent.event === ON_PRESS_EVENT) { + onPress?.(); + return; + } + onPressAction?.(event); + }} ref={ref} /> ); diff --git a/src/types.ts b/src/types.ts index 9c323d9c..e6c44c4d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -115,6 +115,13 @@ type MenuComponentPropsBase = { * It will contain id of the given action. */ onPressAction?: ({ nativeEvent }: NativeActionEvent) => void; + /** + * Callback fired when the anchor is tapped (a normal press, not a long + * press). Only meaningful together with `shouldOpenOnLongPress`: the menu + * opens on long press while a normal tap invokes this — letting the anchor + * be both tappable and long-pressable. + */ + onPress?: () => void; /** * Callback function that will be called when the menu closes. */