From 35308a73a5e0f69bf32d687be52a8f739fbf2e50 Mon Sep 17 00:00:00 2001 From: Ramanpreet Nara Date: Mon, 29 Jan 2024 18:54:14 -0800 Subject: [PATCH] Open source PopupMenuAndroid Summary: In React Native 0.75, we will remove UIManager.showPopupMenu(), UIManager.dismissPopupMenu(). To replace that API, we are introducing this component. This component works in both Fabric and Paper! For the usage, please see PopupMenuAndroidExample.js. Changelog: [Android][Added] - Introduce PopupMenuAndroid to replace UIManager.showPopupMenu() Reviewed By: mdvacca Differential Revision: D52712758 fbshipit-source-id: a87628a168d64fabbcc4d0f7b694fa639a927448 --- .../PopupMenuAndroid.android.js | 69 +++++++++++++++++++ .../PopupMenuAndroid/PopupMenuAndroid.d.ts | 24 +++++++ .../PopupMenuAndroid/PopupMenuAndroid.js | 33 +++++++++ .../PopupMenuAndroidNativeComponent.js | 13 ++++ .../__snapshots__/public-api-test.js.snap | 36 ++++++++++ .../ReactAndroid/api/ReactAndroid.api | 53 ++++++++++++++ .../react/shell/MainReactPackage.java | 3 + .../popupmenu/PopupMenuSelectionEvent.kt | 36 ++++++++++ .../popupmenu/ReactPopupMenuContainer.kt | 50 ++++++++++++++ .../views/popupmenu/ReactPopupMenuManager.kt | 54 +++++++++++++++ .../react/fabric/CoreComponentsRegistry.cpp | 2 + packages/react-native/index.js | 5 ++ .../PopupMenuAndroidNativeComponent.js | 47 +++++++++++++ .../PopupMenuAndroidExample.js | 69 +++++++++++++++++++ .../js/utils/RNTesterList.android.js | 5 ++ 15 files changed, 499 insertions(+) create mode 100644 packages/react-native/Libraries/Components/PopupMenuAndroid/PopupMenuAndroid.android.js create mode 100644 packages/react-native/Libraries/Components/PopupMenuAndroid/PopupMenuAndroid.d.ts create mode 100644 packages/react-native/Libraries/Components/PopupMenuAndroid/PopupMenuAndroid.js create mode 100644 packages/react-native/Libraries/Components/PopupMenuAndroid/PopupMenuAndroidNativeComponent.js create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/popupmenu/PopupMenuSelectionEvent.kt create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/popupmenu/ReactPopupMenuContainer.kt create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/popupmenu/ReactPopupMenuManager.kt create mode 100644 packages/react-native/src/private/specs/components/PopupMenuAndroidNativeComponent.js create mode 100644 packages/rn-tester/js/examples/PopupMenuAndroid/PopupMenuAndroidExample.js diff --git a/packages/react-native/Libraries/Components/PopupMenuAndroid/PopupMenuAndroid.android.js b/packages/react-native/Libraries/Components/PopupMenuAndroid/PopupMenuAndroid.android.js new file mode 100644 index 00000000000000..035a1da5b150d6 --- /dev/null +++ b/packages/react-native/Libraries/Components/PopupMenuAndroid/PopupMenuAndroid.android.js @@ -0,0 +1,69 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow strict-local + */ + +import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes'; +import type {SyntheticEvent} from '../../Types/CoreEventTypes'; +import type {RefObject} from 'react'; + +import PopupMenuAndroidNativeComponent, { + Commands, +} from './PopupMenuAndroidNativeComponent'; +import nullthrows from 'nullthrows'; +import * as React from 'react'; +import {useCallback, useImperativeHandle, useRef} from 'react'; + +type PopupMenuSelectionEvent = SyntheticEvent< + $ReadOnly<{ + item: number, + }>, +>; + +export type PopupMenuAndroidInstance = { + +show: () => void, +}; + +type Props = { + menuItems: $ReadOnlyArray, + onSelectionChange: number => void, + children: React.Node, + instanceRef: RefObject, +}; + +export default function PopupMenuAndroid({ + menuItems, + onSelectionChange, + children, + instanceRef, +}: Props): React.Node { + const nativeRef = useRef> | null>(null); + const _onSelectionChange = useCallback( + (event: PopupMenuSelectionEvent) => { + onSelectionChange(event.nativeEvent.item); + }, + [onSelectionChange], + ); + + useImperativeHandle(instanceRef, ItemViewabilityInstance => { + return { + show() { + Commands.show(nullthrows(nativeRef.current)); + }, + }; + }); + + return ( + + {children} + + ); +} diff --git a/packages/react-native/Libraries/Components/PopupMenuAndroid/PopupMenuAndroid.d.ts b/packages/react-native/Libraries/Components/PopupMenuAndroid/PopupMenuAndroid.d.ts new file mode 100644 index 00000000000000..435df11d487fe7 --- /dev/null +++ b/packages/react-native/Libraries/Components/PopupMenuAndroid/PopupMenuAndroid.d.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +import type * as React from 'react'; +import {HostComponent} from '../../../types/public/ReactNativeTypes'; + +type PopupMenuAndroidInstance = { + show: () => void; +}; + +type Props = { + menuItems: Array; + onSelectionChange: (number) => void; + children: React.ReactNode | undefined; + instanceRef: React.ElementRef>; +}; + +declare class PopupMenuAndroid extends React.Component {} diff --git a/packages/react-native/Libraries/Components/PopupMenuAndroid/PopupMenuAndroid.js b/packages/react-native/Libraries/Components/PopupMenuAndroid/PopupMenuAndroid.js new file mode 100644 index 00000000000000..94677cd6b8d23d --- /dev/null +++ b/packages/react-native/Libraries/Components/PopupMenuAndroid/PopupMenuAndroid.js @@ -0,0 +1,33 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow strict-local + */ + +import type {RefObject} from 'react'; +import type {Node} from 'react'; + +import * as React from 'react'; + +const UnimplementedView = require('../UnimplementedViews/UnimplementedView'); + +export type PopupMenuAndroidInstance = { + +show: () => void, +}; + +type Props = { + menuItems: $ReadOnlyArray, + onSelectionChange: number => void, + children: Node, + instanceRef: RefObject, +}; + +function PopupMenuAndroid(props: Props): Node { + return ; +} + +export default PopupMenuAndroid; diff --git a/packages/react-native/Libraries/Components/PopupMenuAndroid/PopupMenuAndroidNativeComponent.js b/packages/react-native/Libraries/Components/PopupMenuAndroid/PopupMenuAndroidNativeComponent.js new file mode 100644 index 00000000000000..05bbd5693155c5 --- /dev/null +++ b/packages/react-native/Libraries/Components/PopupMenuAndroid/PopupMenuAndroidNativeComponent.js @@ -0,0 +1,13 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow strict-local + */ + +export * from '../../../src/private/specs/components/PopupMenuAndroidNativeComponent'; +import PopupMenuAndroidNativeComponent from '../../../src/private/specs/components/PopupMenuAndroidNativeComponent'; +export default PopupMenuAndroidNativeComponent; diff --git a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap index e0c5b5e73960d2..c0fd87b816944d 100644 --- a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap +++ b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap @@ -1772,6 +1772,41 @@ declare export default typeof NativeKeyboardObserver; " `; +exports[`public API should not change unintentionally Libraries/Components/PopupMenuAndroid/PopupMenuAndroid.android.js 1`] = ` +"export type PopupMenuAndroidInstance = { + +show: () => void, +}; +type Props = { + menuItems: $ReadOnlyArray, + onSelectionChange: (number) => void, + children: React.Node, + instanceRef: RefObject, +}; +declare export default function PopupMenuAndroid(Props): React.Node; +" +`; + +exports[`public API should not change unintentionally Libraries/Components/PopupMenuAndroid/PopupMenuAndroid.js 1`] = ` +"export type PopupMenuAndroidInstance = { + +show: () => void, +}; +type Props = { + menuItems: $ReadOnlyArray, + onSelectionChange: (number) => void, + children: Node, + instanceRef: RefObject, +}; +declare function PopupMenuAndroid(props: Props): Node; +declare export default typeof PopupMenuAndroid; +" +`; + +exports[`public API should not change unintentionally Libraries/Components/PopupMenuAndroid/PopupMenuAndroidNativeComponent.js 1`] = ` +"export * from \\"../../../src/private/specs/components/PopupMenuAndroidNativeComponent\\"; +declare export default typeof PopupMenuAndroidNativeComponent; +" +`; + exports[`public API should not change unintentionally Libraries/Components/Pressable/Pressable.js 1`] = ` "type ViewStyleProp = $ElementType, \\"style\\">; export type StateCallbackType = $ReadOnly<{| @@ -9185,6 +9220,7 @@ declare module.exports: { get ImageBackground(): ImageBackground, get InputAccessoryView(): InputAccessoryView, get KeyboardAvoidingView(): KeyboardAvoidingView, + get PopupMenuAndroid(): PopupMenuAndroid, get Modal(): Modal, get Pressable(): Pressable, get ProgressBarAndroid(): ProgressBarAndroid, diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 7a3a8f2c05b3d4..2a5f753722afab 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -5693,6 +5693,17 @@ public abstract interface class com/facebook/react/viewmanagers/AndroidHorizonta public abstract fun setRemoveClippedSubviews (Landroid/view/View;Z)V } +public class com/facebook/react/viewmanagers/AndroidPopupMenuManagerDelegate : com/facebook/react/uimanager/BaseViewManagerDelegate { + public fun (Lcom/facebook/react/uimanager/BaseViewManagerInterface;)V + public fun receiveCommand (Landroid/view/View;Ljava/lang/String;Lcom/facebook/react/bridge/ReadableArray;)V + public fun setProperty (Landroid/view/View;Ljava/lang/String;Ljava/lang/Object;)V +} + +public abstract interface class com/facebook/react/viewmanagers/AndroidPopupMenuManagerInterface { + public abstract fun setMenuItems (Landroid/view/View;Lcom/facebook/react/bridge/ReadableArray;)V + public abstract fun show (Landroid/view/View;)V +} + public class com/facebook/react/viewmanagers/AndroidProgressBarManagerDelegate : com/facebook/react/uimanager/BaseViewManagerDelegate { public fun (Lcom/facebook/react/uimanager/BaseViewManagerInterface;)V public fun setProperty (Landroid/view/View;Ljava/lang/String;Ljava/lang/Object;)V @@ -6162,6 +6173,48 @@ public abstract interface class com/facebook/react/views/modal/ReactModalHostVie public abstract fun onRequestClose (Landroid/content/DialogInterface;)V } +public final class com/facebook/react/views/popupmenu/PopupMenuSelectionEvent : com/facebook/react/uimanager/events/Event { + public static final field Companion Lcom/facebook/react/views/popupmenu/PopupMenuSelectionEvent$Companion; + public static final field EVENT_NAME Ljava/lang/String; + public fun (III)V + public fun dispatch (Lcom/facebook/react/uimanager/events/RCTEventEmitter;)V + public fun getEventName ()Ljava/lang/String; + public final fun getItem ()I +} + +public final class com/facebook/react/views/popupmenu/PopupMenuSelectionEvent$Companion { +} + +public final class com/facebook/react/views/popupmenu/ReactPopupMenuContainer : android/widget/FrameLayout { + public fun (Landroid/content/Context;)V + public final fun setMenuItems (Lcom/facebook/react/bridge/ReadableArray;)V + public final fun showPopupMenu ()V +} + +public final class com/facebook/react/views/popupmenu/ReactPopupMenuManager : com/facebook/react/uimanager/ViewGroupManager, com/facebook/react/viewmanagers/AndroidPopupMenuManagerInterface { + public static final field Companion Lcom/facebook/react/views/popupmenu/ReactPopupMenuManager$Companion; + public static final field REACT_CLASS Ljava/lang/String; + public fun ()V + public synthetic fun createViewInstance (Lcom/facebook/react/uimanager/ThemedReactContext;)Landroid/view/View; + public fun getName ()Ljava/lang/String; + public synthetic fun receiveCommand (Landroid/view/View;Ljava/lang/String;Lcom/facebook/react/bridge/ReadableArray;)V + public fun receiveCommand (Lcom/facebook/react/views/popupmenu/ReactPopupMenuContainer;Ljava/lang/String;Lcom/facebook/react/bridge/ReadableArray;)V + public synthetic fun setMenuItems (Landroid/view/View;Lcom/facebook/react/bridge/ReadableArray;)V + public fun setMenuItems (Lcom/facebook/react/views/popupmenu/ReactPopupMenuContainer;Lcom/facebook/react/bridge/ReadableArray;)V + public synthetic fun show (Landroid/view/View;)V + public fun show (Lcom/facebook/react/views/popupmenu/ReactPopupMenuContainer;)V +} + +public class com/facebook/react/views/popupmenu/ReactPopupMenuManager$$PropsSetter : com/facebook/react/uimanager/ViewManagerPropertyUpdater$ViewManagerSetter { + public fun ()V + public fun getProperties (Ljava/util/Map;)V + public synthetic fun setProperty (Lcom/facebook/react/uimanager/ViewManager;Landroid/view/View;Ljava/lang/String;Ljava/lang/Object;)V + public fun setProperty (Lcom/facebook/react/views/popupmenu/ReactPopupMenuManager;Lcom/facebook/react/views/popupmenu/ReactPopupMenuContainer;Ljava/lang/String;Ljava/lang/Object;)V +} + +public final class com/facebook/react/views/popupmenu/ReactPopupMenuManager$Companion { +} + public class com/facebook/react/views/progressbar/ProgressBarShadowNode : com/facebook/react/uimanager/LayoutShadowNode, com/facebook/yoga/YogaMeasureFunction { public fun ()V public fun getStyle ()Ljava/lang/String; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java index dac68508acaa0f..b06dcc7d745983 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java @@ -47,6 +47,7 @@ import com.facebook.react.views.drawer.ReactDrawerLayoutManager; import com.facebook.react.views.image.ReactImageManager; import com.facebook.react.views.modal.ReactModalHostManager; +import com.facebook.react.views.popupmenu.ReactPopupMenuManager; import com.facebook.react.views.progressbar.ReactProgressBarViewManager; import com.facebook.react.views.scroll.ReactHorizontalScrollContainerViewManager; import com.facebook.react.views.scroll.ReactHorizontalScrollViewManager; @@ -170,6 +171,7 @@ public List createViewManagers(ReactApplicationContext reactContext viewManagers.add(new ReactScrollViewManager()); viewManagers.add(new ReactSwitchManager()); viewManagers.add(new SwipeRefreshLayoutManager()); + viewManagers.add(new ReactPopupMenuManager()); // Native equivalents viewManagers.add(new FrescoBasedReactTextInlineImageViewManager()); @@ -211,6 +213,7 @@ public Map getViewManagersMap() { appendMap(viewManagers, ReactSwitchManager.REACT_CLASS, ReactSwitchManager::new); appendMap( viewManagers, SwipeRefreshLayoutManager.REACT_CLASS, SwipeRefreshLayoutManager::new); + appendMap(viewManagers, ReactPopupMenuManager.REACT_CLASS, ReactPopupMenuManager::new); appendMap( viewManagers, FrescoBasedReactTextInlineImageViewManager.REACT_CLASS, diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/popupmenu/PopupMenuSelectionEvent.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/popupmenu/PopupMenuSelectionEvent.kt new file mode 100644 index 00000000000000..d639e55122a9ff --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/popupmenu/PopupMenuSelectionEvent.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.views.popupmenu + +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.WritableMap +import com.facebook.react.uimanager.events.Event +import com.facebook.react.uimanager.events.RCTEventEmitter + +class PopupMenuSelectionEvent(surfaceId: Int, viewId: Int, val item: Int) : + Event(surfaceId, viewId) { + + override fun getEventName(): String { + return EVENT_NAME + } + + override fun getEventData(): WritableMap { + val eventData: WritableMap = Arguments.createMap() + eventData.putInt("target", viewTag) + eventData.putDouble("item", item.toDouble()) + return eventData + } + + override fun dispatch(rctEventEmitter: RCTEventEmitter) { + rctEventEmitter.receiveEvent(viewTag, eventName, eventData) + } + + companion object { + const val EVENT_NAME: String = "topSelectionChange" + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/popupmenu/ReactPopupMenuContainer.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/popupmenu/ReactPopupMenuContainer.kt new file mode 100644 index 00000000000000..f3a8fa52457b3c --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/popupmenu/ReactPopupMenuContainer.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.views.popupmenu + +import android.content.Context +import android.os.Build +import android.view.Menu +import android.widget.FrameLayout +import android.widget.PopupMenu +import com.facebook.react.bridge.ReactContext +import com.facebook.react.bridge.ReadableArray +import com.facebook.react.uimanager.UIManagerHelper + +class ReactPopupMenuContainer(context: Context) : FrameLayout(context) { + private var menuItems: ReadableArray? = null + + fun setMenuItems(items: ReadableArray?) { + menuItems = items + } + + fun showPopupMenu() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + val view = getChildAt(0) + val popupMenu = PopupMenu(context, view) + var menu: Menu? = null + menu = popupMenu.menu + val items = menuItems + if (items != null) { + for (i in 0 until items.size()) { + menu.add(Menu.NONE, Menu.NONE, i, items.getString(i)) + } + } + popupMenu.setOnMenuItemClickListener { menuItem -> + val reactContext = context as ReactContext + val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, id) + if (eventDispatcher != null) { + val surfaceId = UIManagerHelper.getSurfaceId(reactContext) + eventDispatcher.dispatchEvent(PopupMenuSelectionEvent(surfaceId, id, menuItem.order)) + } + true + } + popupMenu.show() + } + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/popupmenu/ReactPopupMenuManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/popupmenu/ReactPopupMenuManager.kt new file mode 100644 index 00000000000000..0cff2011933d81 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/popupmenu/ReactPopupMenuManager.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.views.popupmenu + +import com.facebook.react.bridge.ReadableArray +import com.facebook.react.module.annotations.ReactModule +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.ViewGroupManager +import com.facebook.react.uimanager.annotations.ReactProp +import com.facebook.react.viewmanagers.AndroidPopupMenuManagerInterface + +@ReactModule(name = ReactPopupMenuManager.REACT_CLASS) +class ReactPopupMenuManager() : + ViewGroupManager(), + AndroidPopupMenuManagerInterface { + override fun createViewInstance(reactContext: ThemedReactContext): ReactPopupMenuContainer { + return ReactPopupMenuContainer(reactContext) + } + + @ReactProp(name = "menuItems") + override fun setMenuItems(view: ReactPopupMenuContainer, menuItems: ReadableArray?) { + view.setMenuItems(menuItems) + } + + override fun getName(): String { + return REACT_CLASS + } + + override fun receiveCommand( + view: ReactPopupMenuContainer, + commandId: String, + items: ReadableArray? + ) { + when (commandId) { + "show" -> show(view) + else -> { + // no-op + } + } + } + + override fun show(popupMenu: ReactPopupMenuContainer) { + popupMenu.showPopupMenu() + } + + companion object { + const val REACT_CLASS: String = "AndroidPopupMenu" + } +} diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/CoreComponentsRegistry.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/CoreComponentsRegistry.cpp index fd8e579865f73b..fdb2f051621fd2 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/CoreComponentsRegistry.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/CoreComponentsRegistry.cpp @@ -67,6 +67,8 @@ CoreComponentsRegistry::sharedProviderRegistry() { AndroidDrawerLayoutComponentDescriptor>()); providerRegistry->add(concreteComponentDescriptorProvider< DebuggingOverlayComponentDescriptor>()); + providerRegistry->add(concreteComponentDescriptorProvider< + AndroidPopupMenuComponentDescriptor>()); return providerRegistry; }(); diff --git a/packages/react-native/index.js b/packages/react-native/index.js index 270e32924b0d00..a91ef1287313aa 100644 --- a/packages/react-native/index.js +++ b/packages/react-native/index.js @@ -27,6 +27,7 @@ import typeof Clipboard from './Libraries/Components/Clipboard/Clipboard'; import typeof DrawerLayoutAndroid from './Libraries/Components/DrawerAndroid/DrawerLayoutAndroid'; import typeof Keyboard from './Libraries/Components/Keyboard/Keyboard'; import typeof KeyboardAvoidingView from './Libraries/Components/Keyboard/KeyboardAvoidingView'; +import typeof PopupMenuAndroid from './Libraries/Components/PopupMenuAndroid/PopupMenuAndroid'; import typeof Pressable from './Libraries/Components/Pressable/Pressable'; import typeof ProgressBarAndroid from './Libraries/Components/ProgressBarAndroid/ProgressBarAndroid'; import typeof RefreshControl from './Libraries/Components/RefreshControl/RefreshControl'; @@ -133,6 +134,10 @@ module.exports = { return require('./Libraries/Components/Keyboard/KeyboardAvoidingView') .default; }, + get PopupMenuAndroid(): PopupMenuAndroid { + return require('./Libraries/Components/PopupMenuAndroid/PopupMenuAndroid') + .default; + }, get Modal(): Modal { return require('./Libraries/Modal/Modal'); }, diff --git a/packages/react-native/src/private/specs/components/PopupMenuAndroidNativeComponent.js b/packages/react-native/src/private/specs/components/PopupMenuAndroidNativeComponent.js new file mode 100644 index 00000000000000..81801f65f4d0dd --- /dev/null +++ b/packages/react-native/src/private/specs/components/PopupMenuAndroidNativeComponent.js @@ -0,0 +1,47 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +import type {ViewProps} from '../../../../Libraries/Components/View/ViewPropTypes'; +import type {HostComponent} from '../../../../Libraries/Renderer/shims/ReactNativeTypes'; +import type { + DirectEventHandler, + Int32, +} from '../../../../Libraries/Types/CodegenTypes'; + +import codegenNativeCommands from '../../../../Libraries/Utilities/codegenNativeCommands'; +import codegenNativeComponent from '../../../../Libraries/Utilities/codegenNativeComponent'; +import * as React from 'react'; + +type PopupMenuSelectionEvent = $ReadOnly<{ + item: Int32, +}>; + +type NativeProps = $ReadOnly<{ + ...ViewProps, + + //Props + menuItems?: ?$ReadOnlyArray, + + onSelectionChange?: DirectEventHandler, +}>; + +type ComponentType = HostComponent; + +interface NativeCommands { + +show: (viewRef: React.ElementRef) => void; +} + +export const Commands: NativeCommands = codegenNativeCommands({ + supportedCommands: ['show'], +}); + +export default (codegenNativeComponent( + 'AndroidPopupMenu', +): HostComponent); diff --git a/packages/rn-tester/js/examples/PopupMenuAndroid/PopupMenuAndroidExample.js b/packages/rn-tester/js/examples/PopupMenuAndroid/PopupMenuAndroidExample.js new file mode 100644 index 00000000000000..2b35119e662ed8 --- /dev/null +++ b/packages/rn-tester/js/examples/PopupMenuAndroid/PopupMenuAndroidExample.js @@ -0,0 +1,69 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + */ + +'use strict'; + +import type {Node} from 'react'; +import type {PopupMenuAndroidInstance} from 'react-native/Libraries/Components/PopupMenuAndroid/PopupMenuAndroid'; + +import * as React from 'react'; +import {Button, PopupMenuAndroid, StyleSheet, Text, View} from 'react-native'; + +type Fruit = 'Apple' | 'Pear' | 'Banana' | 'Orange' | 'Kiwi'; + +const PopupMenu = () => { + const popupRef = React.useRef(); + const [fruit, setFruit] = React.useState(); + const fruits: Array = ['Apple', 'Pear', 'Banana', 'Orange', 'Kiwi']; + const items = fruits.map(item => ({ + label: item, + onPress: () => { + setFruit(item); + }, + })); + + return ( + + {fruit ? Selected {fruit} : null} + label)} + onSelectionChange={selection => items[selection].onPress()}> +