Skip to content

Commit

Permalink
Open source PopupMenuAndroid
Browse files Browse the repository at this point in the history
Summary:
In React Native 0.75, we will remove UIManager.showPopupMenu(), UIManager.dismissPopupMenu().

To replace that API, we are introducing this <PopupMenuAndroid> 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
  • Loading branch information
RSNara authored and facebook-github-bot committed Jan 30, 2024
1 parent 7f549ec commit 35308a7
Show file tree
Hide file tree
Showing 15 changed files with 499 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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<string>,
onSelectionChange: number => void,
children: React.Node,
instanceRef: RefObject<?PopupMenuAndroidInstance>,
};

export default function PopupMenuAndroid({
menuItems,
onSelectionChange,
children,
instanceRef,
}: Props): React.Node {
const nativeRef = useRef<React.ElementRef<HostComponent<mixed>> | null>(null);
const _onSelectionChange = useCallback(
(event: PopupMenuSelectionEvent) => {
onSelectionChange(event.nativeEvent.item);
},
[onSelectionChange],
);

useImperativeHandle(instanceRef, ItemViewabilityInstance => {
return {
show() {
Commands.show(nullthrows(nativeRef.current));
},
};
});

return (
<PopupMenuAndroidNativeComponent
ref={nativeRef}
onSelectionChange={_onSelectionChange}
menuItems={menuItems}>
{children}
</PopupMenuAndroidNativeComponent>
);
}
Original file line number Diff line number Diff line change
@@ -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<string>;
onSelectionChange: (number) => void;
children: React.ReactNode | undefined;
instanceRef: React.ElementRef<HostComponent<PopupMenuAndroidInstance>>;
};

declare class PopupMenuAndroid extends React.Component<Props> {}
Original file line number Diff line number Diff line change
@@ -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<string>,
onSelectionChange: number => void,
children: Node,
instanceRef: RefObject<?PopupMenuAndroidInstance>,
};

function PopupMenuAndroid(props: Props): Node {
return <UnimplementedView />;
}

export default PopupMenuAndroid;
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>,
onSelectionChange: (number) => void,
children: React.Node,
instanceRef: RefObject<?PopupMenuAndroidInstance>,
};
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<string>,
onSelectionChange: (number) => void,
children: Node,
instanceRef: RefObject<?PopupMenuAndroidInstance>,
};
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<React.ElementConfig<typeof View>, \\"style\\">;
export type StateCallbackType = $ReadOnly<{|
Expand Down Expand Up @@ -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,
Expand Down
53 changes: 53 additions & 0 deletions packages/react-native/ReactAndroid/api/ReactAndroid.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 <init> (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 <init> (Lcom/facebook/react/uimanager/BaseViewManagerInterface;)V
public fun setProperty (Landroid/view/View;Ljava/lang/String;Ljava/lang/Object;)V
Expand Down Expand Up @@ -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 <init> (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 <init> (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 <init> ()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 <init> ()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 <init> ()V
public fun getStyle ()Ljava/lang/String;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -170,6 +171,7 @@ public List<ViewManager> 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());
Expand Down Expand Up @@ -211,6 +213,7 @@ public Map<String, ModuleSpec> 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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<PopupMenuSelectionEvent>(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"
}
}
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
Loading

0 comments on commit 35308a7

Please sign in to comment.