From b36fc8e02aea1936441fdc509727c93f2f01cd20 Mon Sep 17 00:00:00 2001 From: Peter Abbondanzo Date: Wed, 23 Apr 2025 09:03:22 -0700 Subject: [PATCH] Add prop to filter drag and drop pasting on Android (#49446) Summary: On Android, by default, every EditText accepts `DragEvent` and will automatically focus themselves to accept these data. In some rare cases, it might not be desirable to allow data from arbitrary drag and drop events to be pasted into a text input. This change adds a new prop `acceptDragAndDropTypes` to do exactly that: reject drag and drop events by telling the system to ignore certain types of drag data and, by proxy, disabling behavior that automatically focuses the text input. The prop accepts a subset of MIME types supported by Android as documented [here](https://developer.android.com/reference/android/content/ClipDescription#MIMETYPE_TEXT_HTML). It's important to note that this is an experimental prop, as is evident by the `experimental_` prefix on the JS side. Its signature could change before the prop has fully matured, use at your own risk Changelog: [Android][Added] - Add new prop for filtering drag and drop targeting to text inputs Reviewed By: javache Differential Revision: D69674225 --- .../AndroidTextInputNativeComponent.js | 1 + .../Components/TextInput/TextInput.flow.js | 13 ++++++++++++ .../Components/TextInput/TextInput.js | 1 + .../__snapshots__/public-api-test.js.snap | 1 + .../ReactAndroid/api/ReactAndroid.api | 4 ++++ .../react/views/textinput/ReactEditText.kt | 13 ++++++++++++ .../views/textinput/ReactTextInputManager.kt | 16 ++++++++++++++ .../TextInput/TextInputExample.android.js | 21 +++++++++++++++++++ 8 files changed, 70 insertions(+) diff --git a/packages/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js b/packages/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js index 734b37d3dc3644..ef6296d1c81006 100644 --- a/packages/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js +++ b/packages/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js @@ -658,6 +658,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = { }, }, validAttributes: { + acceptDragAndDropTypes: true, maxFontSizeMultiplier: true, adjustsFontSizeToFit: true, minimumFontScale: true, diff --git a/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js b/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js index b273341b328ee2..a97736ded0c9c9 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js @@ -405,6 +405,19 @@ export type TextInputIOSProps = $ReadOnly<{ }>; export type TextInputAndroidProps = $ReadOnly<{ + /** + * When provided, the text input will only accept drag and drop events for the specified + * mime types. If null or not provided, the text input will accept all types of drag and drop + * events. + * Defaults to null. + * + * *NOTE*: This prop is experimental and its API may change in the future. Use at your own risk. + * + * @platform android + * @see https://developer.android.com/reference/android/content/ClipData for more information on MIME types + */ + experimental_acceptDragAndDropTypes?: ?$ReadOnlyArray, + /** * When provided it will set the color of the cursor (or "caret") in the component. * Unlike the behavior of `selectionColor` the cursor color will be set independently diff --git a/packages/react-native/Libraries/Components/TextInput/TextInput.js b/packages/react-native/Libraries/Components/TextInput/TextInput.js index b56c388d88c9b4..f95bec49f9a4cf 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.js +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.js @@ -741,6 +741,7 @@ function InternalTextInput(props: TextInputProps): React.Node { accessibilityState={_accessibilityState} accessibilityLabelledBy={_accessibilityLabelledBy} accessible={accessible} + acceptDragAndDropTypes={props.experimental_acceptDragAndDropTypes} autoCapitalize={autoCapitalize} submitBehavior={submitBehavior} caretHidden={caretHidden} 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 d64601d1560022..aeefeec68c2d40 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 @@ -2831,6 +2831,7 @@ export type TextInputIOSProps = $ReadOnly<{ smartInsertDelete?: ?boolean, }>; export type TextInputAndroidProps = $ReadOnly<{ + experimental_acceptDragAndDropTypes?: ?$ReadOnlyArray, cursorColor?: ?ColorValue, selectionHandleColor?: ?ColorValue, disableFullscreenUI?: ?boolean, diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 67a6a658e336ed..dd7804c781f99d 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -6650,6 +6650,7 @@ public class com/facebook/react/views/textinput/ReactEditText : androidx/appcomp public final fun getBorderColor (I)I protected final fun getContainsImages ()Z public final fun getDisableFullscreenUI ()Z + public final fun getDragAndDropFilter ()Ljava/util/List; protected final fun getNativeEventCount ()I public final fun getReturnKeyType ()Ljava/lang/String; public final fun getStagedInputType ()I @@ -6669,6 +6670,7 @@ public class com/facebook/react/views/textinput/ReactEditText : androidx/appcomp public fun onConfigurationChanged (Landroid/content/res/Configuration;)V public fun onCreateInputConnection (Landroid/view/inputmethod/EditorInfo;)Landroid/view/inputmethod/InputConnection; public fun onDetachedFromWindow ()V + public fun onDragEvent (Landroid/view/DragEvent;)Z public fun onDraw (Landroid/graphics/Canvas;)V public fun onFinishTemporaryDetach ()V protected fun onFocusChanged (ZILandroid/graphics/Rect;)V @@ -6694,6 +6696,7 @@ public class com/facebook/react/views/textinput/ReactEditText : androidx/appcomp public final fun setContentSizeWatcher (Lcom/facebook/react/views/textinput/ContentSizeWatcher;)V public final fun setContextMenuHidden (Z)V public final fun setDisableFullscreenUI (Z)V + public final fun setDragAndDropFilter (Ljava/util/List;)V public final fun setEventDispatcher (Lcom/facebook/react/uimanager/events/EventDispatcher;)V public final fun setFontFamily (Ljava/lang/String;)V public fun setFontFeatureSettings (Ljava/lang/String;)V @@ -6757,6 +6760,7 @@ public class com/facebook/react/views/textinput/ReactTextInputManager : com/face public synthetic fun receiveCommand (Landroid/view/View;Ljava/lang/String;Lcom/facebook/react/bridge/ReadableArray;)V public fun receiveCommand (Lcom/facebook/react/views/textinput/ReactEditText;ILcom/facebook/react/bridge/ReadableArray;)V public fun receiveCommand (Lcom/facebook/react/views/textinput/ReactEditText;Ljava/lang/String;Lcom/facebook/react/bridge/ReadableArray;)V + public final fun setAcceptDragAndDropTypes (Lcom/facebook/react/views/textinput/ReactEditText;Lcom/facebook/react/bridge/ReadableArray;)V public final fun setAllowFontScaling (Lcom/facebook/react/views/textinput/ReactEditText;Z)V public final fun setAutoCapitalize (Lcom/facebook/react/views/textinput/ReactEditText;Lcom/facebook/react/bridge/Dynamic;)V public final fun setAutoCorrect (Lcom/facebook/react/views/textinput/ReactEditText;Ljava/lang/Boolean;)V diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt index 3c9e4cbba28dc1..2936dd005c8c2a 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt @@ -28,6 +28,7 @@ import android.text.method.KeyListener import android.text.method.QwertyKeyListener import android.util.TypedValue import android.view.ActionMode +import android.view.DragEvent import android.view.Gravity import android.view.KeyEvent import android.view.Menu @@ -121,6 +122,7 @@ public open class ReactEditText public constructor(context: Context) : AppCompat public var stagedInputType: Int protected var containsImages: Boolean = false public var submitBehavior: String? = null + public var dragAndDropFilter: List? = null private var disableFullscreen: Boolean private var selectionWatcher: SelectionWatcher? = null @@ -1192,6 +1194,17 @@ public open class ReactEditText public constructor(context: Context) : AppCompat super.onDraw(canvas) } + public override fun onDragEvent(event: DragEvent): Boolean { + val dragFilter = dragAndDropFilter + if (dragFilter != null && event.action == DragEvent.ACTION_DRAG_STARTED) { + val shouldHandle = dragFilter.any { filter -> event.clipDescription.hasMimeType(filter) } + if (!shouldHandle) { + return false + } + } + return super.onDragEvent(event) + } + /** * This class will redirect *TextChanged calls to the listeners only in the case where the text is * changed by the user, and not explicitly set by JS. diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt index cc0a42197f1b21..373f6c673512fa 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt @@ -770,6 +770,22 @@ public open class ReactTextInputManager public constructor() : view.returnKeyType = returnKeyType } + @ReactProp(name = "acceptDragAndDropTypes") + public fun setAcceptDragAndDropTypes( + view: ReactEditText, + acceptDragAndDropTypes: ReadableArray? + ) { + if (acceptDragAndDropTypes == null) { + view.dragAndDropFilter = null + } else { + val acceptedTypes = mutableListOf() + for (i in 0 until acceptDragAndDropTypes.size()) { + acceptDragAndDropTypes.getString(i)?.also(acceptedTypes::add) + } + view.dragAndDropFilter = acceptedTypes + } + } + @ReactProp(name = "disableFullscreenUI", defaultBoolean = false) public fun setDisableFullscreenUI(view: ReactEditText, disableFullscreenUI: Boolean) { view.disableFullscreenUI = disableFullscreenUI diff --git a/packages/rn-tester/js/examples/TextInput/TextInputExample.android.js b/packages/rn-tester/js/examples/TextInput/TextInputExample.android.js index 948635ff940109..551e4277ce7b92 100644 --- a/packages/rn-tester/js/examples/TextInput/TextInputExample.android.js +++ b/packages/rn-tester/js/examples/TextInput/TextInputExample.android.js @@ -448,6 +448,27 @@ const examples: Array = [ return ; }, }, + { + title: 'Drag and drop', + render: function (): React.Node { + return ( + + + + + + ); + }, + }, ]; module.exports = ({