The direction is incorrect as it to the right side, which extends outside of the screen. I need it to open to the left side since the trigger is located on the right edge of the header (Refer to the picture)
ThreeDotMenu
component into the right side your header.These codes are sourced from the example section of the Zeego documentation.
import PressableButton from "@/components/pressable/Button";
import * as Menu from "zeego/dropdown-menu";
import MenuIcon from "assets/icons/menu-dot.svg";
export default function ThreeDotMenu() {
return (
<Menu.Root>
<Menu.Trigger asChild>
<PressableButton className="flex flex-row items-center p-2.5" wrapperClassName="rounded-full">
<MenuIcon height={20} width={20} />
</PressableButton>
</Menu.Trigger>
<Menu.Content>
<Menu.Label>Menu</Menu.Label>
<Menu.Item key="item1">
<Menu.ItemTitle>Item 1</Menu.ItemTitle>
</Menu.Item>
<Menu.Group>
<Menu.Item key="item2">
<Menu.ItemTitle>Item 2</Menu.ItemTitle>
</Menu.Item>
</Menu.Group>
<Menu.CheckboxItem key="item3" value="off">
<Menu.ItemTitle>Item 3</Menu.ItemTitle>
<Menu.ItemIndicator />
</Menu.CheckboxItem>
<Menu.Sub>
<Menu.SubTrigger key="item4">
<Menu.ItemTitle>Item 4</Menu.ItemTitle>
</Menu.SubTrigger>
<Menu.SubContent>
<Menu.Item key="item5">
<Menu.ItemTitle>Item 5</Menu.ItemTitle>
</Menu.Item>
</Menu.SubContent>
</Menu.Sub>
<Menu.Separator />
<Menu.Arrow />
</Menu.Content>
</Menu.Root>
);
}
This is the outcome of my project operating on the Android platform.
Originally posted by @sumittttpaul in #113
Need to patch some native implementations
npm install patch-package --save-dev
or if you use Yarn
yarn add patch-package --dev
"scripts": {
"postinstall": "patch-package"
}
This script will automatically apply your patch every time you run npm install
or yarn
.
node_modules/@react-native-menu/menu/android/src/main/java/com/reactnativemenu/MenuView.kt
Copy and paste the following code
package com.reactnativemenu
import android.content.res.ColorStateList
import android.content.res.Resources
import android.graphics.Color
import android.graphics.Rect
import android.os.Build
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.style.ForegroundColorSpan
import android.view.*
import androidx.appcompat.widget.PopupMenu
import com.facebook.react.bridge.*
import com.facebook.react.uimanager.UIManagerHelper
import com.facebook.react.views.view.ReactViewGroup
import java.lang.reflect.Field
import com.facebook.react.bridge.ReactContext
class MenuView(private val mContext: ReactContext) : ReactViewGroup(mContext) {
private lateinit var mActions: ReadableArray
private var mIsAnchoredToRight = false
private var mPopupMenu: PopupMenu? = null
private var mIsMenuDisplayed = false
private var mIsOnLongPress = false
private var mGestureDetector: GestureDetector
private var mHitSlopRect: Rect? = null
init {
mGestureDetector = GestureDetector(mContext, object : GestureDetector.SimpleOnGestureListener() {
override fun onLongPress(e: MotionEvent) {
if (!mIsOnLongPress) {
return
}
prepareMenu()
}
override fun onSingleTapUp(e: MotionEvent): Boolean {
if (!mIsOnLongPress) {
prepareMenu()
}
return true
}
})
}
fun show(){
prepareMenu()
}
override fun setHitSlopRect(rect: Rect?) {
super.setHitSlopRect(rect)
mHitSlopRect = rect
updateTouchDelegate()
}
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
return true
}
override fun onTouchEvent(ev: MotionEvent): Boolean {
mGestureDetector.onTouchEvent(ev)
return true
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
updateTouchDelegate()
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
updateTouchDelegate()
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
if (mIsMenuDisplayed) {
mPopupMenu?.dismiss()
}
}
fun setActions(actions: ReadableArray) {
mActions = actions
}
fun setIsAnchoredToRight(isAnchoredToRight: Boolean) {
if (mIsAnchoredToRight == isAnchoredToRight) {
return
}
mIsAnchoredToRight = isAnchoredToRight
}
fun setIsOpenOnLongPress(isLongPress: Boolean) {
mIsOnLongPress = isLongPress
}
private val getActionsCount: Int
get() = mActions.size()
private fun prepareMenuItem(menuItem: MenuItem, config: ReadableMap?) {
val titleColor = when (config != null && config.hasKey("titleColor") && !config.isNull("titleColor")) {
true -> config.getInt("titleColor")
else -> null
}
val imageName = when (config != null && config.hasKey("image") && !config.isNull("image")) {
true -> config.getString("image")
else -> null
}
val imageColor = when (config != null && config.hasKey("imageColor") && !config.isNull("imageColor")) {
true -> config.getInt("imageColor")
else -> null
}
val attributes = when (config != null && config.hasKey("attributes") && !config.isNull(("attributes"))) {
true -> config.getMap("attributes")
else -> null
}
val subactions = when (config != null && config.hasKey("subactions") && !config.isNull(("subactions"))) {
true -> config.getArray("subactions")
else -> null
}
val menuState = config?.getString("state")
if (titleColor != null) {
menuItem.title = getMenuItemTextWithColor(menuItem.title.toString(), titleColor)
}
if (imageName != null) {
val resourceId: Int = getDrawableIdWithName(imageName)
if (resourceId != 0) {
val icon = resources.getDrawable(resourceId, context.theme)
if (imageColor != null) {
icon.setTintList(ColorStateList.valueOf(imageColor))
}
menuItem.icon = icon
}
}
if (attributes != null) {
// actions.attributes.disabled
val disabled = when (attributes.hasKey("disabled") && !attributes.isNull("disabled")) {
true -> attributes.getBoolean("disabled")
else -> false
}
menuItem.isEnabled = !disabled
if (!menuItem.isEnabled) {
val disabledColor = 0x77888888
menuItem.title = getMenuItemTextWithColor(menuItem.title.toString(), disabledColor)
if (imageName != null) {
val icon = menuItem.icon
icon?.setTintList(ColorStateList.valueOf(disabledColor))
menuItem.icon = icon
}
}
// actions.attributes.hidden
val hidden = when (attributes.hasKey("hidden") && !attributes.isNull("hidden")) {
true -> attributes.getBoolean("hidden")
else -> false
}
menuItem.isVisible = !hidden
// actions.attributes.destructive
val destructive = when (attributes.hasKey("destructive") && !attributes.isNull("destructive")) {
true -> attributes.getBoolean("destructive")
else -> false
}
if (destructive) {
menuItem.title = getMenuItemTextWithColor(menuItem.title.toString(), Color.RED)
if (imageName != null) {
val icon = menuItem.icon
icon?.setTintList(ColorStateList.valueOf(Color.RED))
menuItem.icon = icon
}
}
}
// Handle menuState for checkable items
when (menuState) {
"on", "off" -> {
menuItem.isCheckable = true
menuItem.isChecked = menuState == "on"
}
else -> menuItem.isCheckable = false
}
// On Android SubMenu cannot contain another SubMenu, so even if there are subactions provided
// we are checking if item has submenu (which will occur only for 1 lvl nesting)
if (subactions != null && menuItem.hasSubMenu()) {
var i = 0
val subactionsCount = subactions.size()
while (i < subactionsCount) {
if (!subactions.isNull(i)) {
val subMenuConfig = subactions.getMap(i)
val subMenuItem = menuItem.subMenu?.add(Menu.NONE, Menu.NONE, i, subMenuConfig?.getString("title"))
if (subMenuItem != null) {
prepareMenuItem(subMenuItem, subMenuConfig)
subMenuItem.setOnMenuItemClickListener {
if (!it.hasSubMenu()) {
mIsMenuDisplayed = false
if (!subactions.isNull(it.order)) {
val selectedItem = subactions.getMap(it.order)
val dispatcher =
UIManagerHelper.getEventDispatcherForReactTag(mContext, id)
val surfaceId: Int = UIManagerHelper.getSurfaceId(this)
dispatcher?.dispatchEvent(
MenuOnPressActionEvent(surfaceId, id, selectedItem?.getString("id"), id)
)
}
true
} else {
false
}
}
}
}
i++
}
}
}
// ####### EDITED FUNCTION ########
private fun prepareMenu() {
if (getActionsCount > 0) {
// Create a new PopupMenu instance each time and anchor it to this view
val popup = PopupMenu(mContext, this)
mPopupMenu = popup
// Set gravity to END to align the right edge of the menu with the right edge of the trigger view
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
popup.gravity = Gravity.END
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
popup.setForceShowIcon(true)
}
var i = 0
while (i < getActionsCount) {
if (!mActions.isNull(i)) {
val item = mActions.getMap(i)
val menuItem = when (item != null && item.hasKey("subactions") && !item.isNull("subactions")) {
true -> popup.menu.addSubMenu(Menu.NONE, Menu.NONE, i, item.getString("title")).item
else -> popup.menu.add(Menu.NONE, Menu.NONE, i, item?.getString("title"))
}
prepareMenuItem(menuItem, item)
menuItem.setOnMenuItemClickListener {
if (!it.hasSubMenu()) {
mIsMenuDisplayed = false
if (!mActions.isNull(it.order)) {
val selectedItem = mActions.getMap(it.order)
val dispatcher =
UIManagerHelper.getEventDispatcherForReactTag(mContext, id)
val surfaceId: Int = UIManagerHelper.getSurfaceId(this)
dispatcher?.dispatchEvent(
MenuOnPressActionEvent(surfaceId, id, selectedItem?.getString("id"), id)
)
}
true
} else {
false
}
}
}
i++
}
popup.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))
popup.show()
}
}
private fun updateTouchDelegate() {
post {
val hitRect = Rect()
getHitRect(hitRect)
mHitSlopRect?.let {
hitRect.left -= it.left
hitRect.top -= it.top
hitRect.right += it.right
hitRect.bottom += it.bottom
}
(parent as? ViewGroup)?.let {
it.touchDelegate = TouchDelegate(hitRect, this)
}
}
}
private fun getDrawableIdWithName(name: String): Int {
val appResources: Resources = context.resources
var resourceId = appResources.getIdentifier(name, "drawable", context.packageName)
if (resourceId == 0) {
// If drawable is not present in app's resources, check system's resources
resourceId = getResId(name, android.R.drawable::class.java)
}
return resourceId
}
private fun getResId(resName: String?, c: Class<*>): Int {
return try {
val idField: Field = c.getDeclaredField(resName!!)
idField.getInt(idField)
} catch (e: Exception) {
e.printStackTrace()
0
}
}
private fun getMenuItemTextWithColor(text: String, color: Int): SpannableStringBuilder {
val textWithColor = SpannableStringBuilder()
textWithColor.append(text)
textWithColor.setSpan(ForegroundColorSpan(color),
0, text.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
return textWithColor
}
}
First patch the @react-native-menu/menu
npx patch-package @react-native-menu/menu
It's best to start with a clean state
If you are using Android Studio
npx expo prebuild --clean
If you are using EAS Build (Only for android using development build)
eas build --platform android --profileĀ development