diff --git a/README.md b/README.md index 04b000c8..abb2b652 100644 --- a/README.md +++ b/README.md @@ -177,13 +177,21 @@ Actions to be displayed in the menu. |--------------|----------| | MenuAction[] | Yes | -### `themeVariant` (iOS only) +### `themeVariant` String to override theme of the menu. If you want to control theme universally across your app, [see this package](https://github.com/vonovak/react-native-theme-control). -| Type | Required | -|-----------------------|----------| -| enum('light', 'dark') | No | +| Type | Required | +| ------------------------------- | -------- | +| enum('light', 'dark', 'system') | No | + +### `uiKit` (Android only) + +String to override UI kit of the menu. Allows you to choose between different Android UI implementations. + +| Type | Required | +| -------------------------------------- | -------- | +| enum('auto', 'material3', 'appcompat') | No | #### `MenuAction` diff --git a/android/build.gradle b/android/build.gradle index f1d2631f..dc531525 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -158,4 +158,4 @@ if (isNewArchitectureEnabled()) { libraryName = "MenuView" codegenJavaPackageName = "com.reactnativemenu" } -} +} \ No newline at end of file diff --git a/android/src/main/java/com/reactnativemenu/MenuView.kt b/android/src/main/java/com/reactnativemenu/MenuView.kt index 17ed7c64..33999bf4 100644 --- a/android/src/main/java/com/reactnativemenu/MenuView.kt +++ b/android/src/main/java/com/reactnativemenu/MenuView.kt @@ -13,13 +13,18 @@ import android.widget.PopupMenu import com.facebook.react.bridge.* import com.facebook.react.uimanager.UIManagerHelper import com.facebook.react.views.view.ReactViewGroup +import com.reactnativemenu.ThemeHelper.appcompat +import com.reactnativemenu.ThemeHelper.isNight +import com.reactnativemenu.ThemeHelper.material3 import java.lang.reflect.Field class MenuView(private val mContext: ReactContext) : ReactViewGroup(mContext) { private lateinit var mActions: ReadableArray + private var uiKit = "auto" + private var themeVariant = "system" private var mIsAnchoredToRight = false - private val mPopupMenu: PopupMenu = PopupMenu(context, this) + private var mPopupMenu: PopupMenu = PopupMenu(context, this) private var mIsMenuDisplayed = false private var mIsOnLongPress = false private var mGestureDetector: GestureDetector @@ -43,7 +48,7 @@ class MenuView(private val mContext: ReactContext) : ReactViewGroup(mContext) { }) } - fun show(){ + fun show() { prepareMenu() } @@ -94,6 +99,18 @@ class MenuView(private val mContext: ReactContext) : ReactViewGroup(mContext) { mIsOnLongPress = isLongPress } + fun setUiKit(uiKit: String?) { + this.uiKit = uiKit ?: "auto" + + setStyle() + } + + fun setThemeVariant(themeVariant: String?) { + this.themeVariant = themeVariant ?: "system" + + setStyle() + } + private val getActionsCount: Int get() = mActions.size() @@ -318,4 +335,22 @@ class MenuView(private val mContext: ReactContext) : ReactViewGroup(mContext) { 0, text.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) return textWithColor } + + private fun setStyle() { + val dark = when (this.themeVariant) { + "dark" -> true + "light" -> false + else -> isNight(context) + } + + val theme = when (this.uiKit) { + "material3" -> material3(dark).takeIf { it != 0 } ?: appcompat(dark) + "appcompat" -> appcompat(dark) + else -> material3(dark).takeIf { it != 0 } ?: appcompat(dark) + } + + val themedCtx = ContextThemeWrapper(context, theme) + + mPopupMenu = PopupMenu(themedCtx, this) + } } diff --git a/android/src/main/java/com/reactnativemenu/MenuViewManagerBase.kt b/android/src/main/java/com/reactnativemenu/MenuViewManagerBase.kt index 4731e1a1..aac7218e 100644 --- a/android/src/main/java/com/reactnativemenu/MenuViewManagerBase.kt +++ b/android/src/main/java/com/reactnativemenu/MenuViewManagerBase.kt @@ -214,6 +214,16 @@ abstract class MenuViewManagerBase : ReactClippingViewManager() { view.setBackfaceVisibility(backfaceVisibility) } + @ReactProp(name = "themeVariant") + fun setThemeVariant(view: MenuView, themeVariant: String?) { + view.setThemeVariant(themeVariant) + } + + @ReactProp(name = "uiKit") + fun setUiKit(view: MenuView, uiKit: String?) { + view.setUiKit(uiKit) + } + override fun setOpacity(@NonNull view: MenuView, opacity: Float) { view.setOpacityIfPossible(opacity) } diff --git a/android/src/main/java/com/reactnativemenu/ThemeHelper.kt b/android/src/main/java/com/reactnativemenu/ThemeHelper.kt new file mode 100644 index 00000000..5299382d --- /dev/null +++ b/android/src/main/java/com/reactnativemenu/ThemeHelper.kt @@ -0,0 +1,33 @@ +package com.reactnativemenu + +import android.content.Context +import android.content.res.Configuration + +object ThemeHelper { + fun isNight(context: Context): Boolean { + val mask = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + return mask == Configuration.UI_MODE_NIGHT_YES + } + + private fun styleIdOrZero(className: String, fieldName: String): Int { + return try { + val cls = Class.forName(className) // exp: "com.google.android.material.R$style" + val f = cls.getField(fieldName) // exp: "ThemeOverlay_Material3_Dark" + f.getInt(null) + } catch (_: Throwable) { + 0 + } + } + + fun material3(dark: Boolean): Int { + // Reflection ile: Material3 varsa al, yoksa 0 döner + val name = if (dark) "ThemeOverlay_Material3_Dark" else "ThemeOverlay_Material3_Light" + return styleIdOrZero("com.google.android.material.R\$style", name) + } + + fun appcompat(dark: Boolean): Int { + // AppCompat reflection (RN projelerinde zaten var) + val name = if (dark) "ThemeOverlay_AppCompat_Dark" else "ThemeOverlay_AppCompat_Light" + return styleIdOrZero("androidx.appcompat.R\$style", name) + } +} diff --git a/example/src/App.tsx b/example/src/App.tsx index 5d43c174..ca1cb911 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,10 +1,14 @@ import * as React from "react"; import { Button, Platform, StyleSheet, Text, View } from "react-native"; -import { MenuView, type MenuComponentRef } from "@react-native-menu/menu"; +import { + MenuView, + type MenuComponentRef, + type MenuThemeVariant, +} from "@react-native-menu/menu"; import { useRef } from "react"; export const App = () => { - const [themeVariant] = React.useState("light"); + const [themeVariant] = React.useState("light"); const menuRef = useRef(null); return ( diff --git a/src/NativeModuleSpecs/UIMenuNativeComponent.ts b/src/NativeModuleSpecs/UIMenuNativeComponent.ts index e6509355..f16a4394 100644 --- a/src/NativeModuleSpecs/UIMenuNativeComponent.ts +++ b/src/NativeModuleSpecs/UIMenuNativeComponent.ts @@ -52,6 +52,7 @@ export interface NativeProps extends ViewProps { actionsHash: string; // just a workaround to make sure we don't have to manually compare MenuActions manually in C++ (since it's a struct and that's a pain) title?: string; themeVariant?: string; + uiKit?: string; shouldOpenOnLongPress?: boolean; hitSlop: { top: Int32; diff --git a/src/index.tsx b/src/index.tsx index a5362981..7fc7f89b 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -8,6 +8,8 @@ import type { ProcessedMenuAction, NativeActionEvent, MenuComponentRef, + MenuThemeVariant, + MenuUiKit, } from "./types"; import { objectHash } from "./utils"; @@ -49,4 +51,6 @@ export type { MenuComponentRef, MenuAction, NativeActionEvent, + MenuThemeVariant, + MenuUiKit, }; diff --git a/src/types.ts b/src/types.ts index 9c323d9c..8fe54046 100644 --- a/src/types.ts +++ b/src/types.ts @@ -12,6 +12,10 @@ export type NativeActionEvent = { }; }; +export type MenuThemeVariant = "light" | "dark" | "system"; + +export type MenuUiKit = "auto" | "material3" | "appcompat"; + type MenuAttributes = { /** * An attribute indicating the destructive style. @@ -147,11 +151,15 @@ type MenuComponentPropsBase = { shouldOpenOnLongPress?: boolean; /** * Overrides theme variant of menu to light mode, dark mode or system theme - * (Only support iOS for now) - * - * @platform iOS + * @default system + */ + themeVariant?: MenuThemeVariant; + /** + * Overrides UI kit of menu to auto, material3 or appcompat + * @platform Android + * @default auto */ - themeVariant?: string; + uiKit?: MenuUiKit; /** * Custom OpenSpace hitSlop prop. Works like touchable hitslop. * @platform iOS