Skip to content

Commit

Permalink
Remove option to paste rich text from Android EditText context menu (#…
Browse files Browse the repository at this point in the history
…38189)

Summary:
Text is copy pasted as rich text on Android TextInput instead of Plain Text.

### What is the root cause of that problem?

Android EditText and iOS UITextField/UITextView have different copy/paste behavior.
- Android TextInput copies/pastes rich text
- iOS UITextField copies/pastes plain text.

| iOS (react-native)   | Android (react-native) |
| ----------- | ----------- |
| <video src="https://user-images.githubusercontent.com/24992535/249170968-8fde35f0-a53c-4c5c-bd89-ee822c08eadf.mp4" width="350" />      | <video src="https://user-images.githubusercontent.com/24992535/249171968-bf0915a0-4060-4586-b267-7c2b463d76f6.mov" width="350" />       |

### What changes do you think we should make in order to solve the problem?

The issue is a bug in react-native #31442:

1) The JavaScript TextInput and ReactEditText Android state are not in sync
2) The TextInput Android Native state over-rides the JavaScript state.
3) onChangeText passes a plain text string to JavaScript, not rich text (text with spans and styles).

More info at Expensify/App#21411 (comment)

The solution consists of:

1) **Over-riding onTextContextMenuItem in ReactEditText to copy/paste plain text instead of rich-text** (https://stackoverflow.com/a/45319485/7295772).
2) **Removing the `Paste as plaintext` option from the insert and selection context menu**

fixes #31442

## Changelog:

[ANDROID] [FIXED] - Remove option to paste rich text from Android EditText context menu

Pull Request resolved: #38189

Test Plan:
#### Reproducing the issue on Android

https://user-images.githubusercontent.com/24992535/249185416-76f8a687-1aca-4dc9-9abe-3d73d6e2893c.mp4

#### Fixing the issue on Android
Sourcecode https://github.com/fabriziobertoglio1987/text-input-cursor-flickering-android/blob/fix-copy-paste/app/src/main/java/com/example/myapplication/CustomEditText.java

https://user-images.githubusercontent.com/24992535/249486339-95449bb9-71b6-430c-8207-f5672f034fa9.mp4

#### Testing the solution on React Native

https://github.com/Expensify/App/assets/24992535/b302237b-99e5-44a2-996d-8bc50bbbc95c

Reviewed By: mdvacca

Differential Revision: D47824730

Pulled By: NickGerleman

fbshipit-source-id: 35525e7d52e664b0f78649d23941262ee45a00cd
  • Loading branch information
fabOnReact authored and facebook-github-bot committed Jul 28, 2023
1 parent 54d70cf commit b1ceea4
Showing 1 changed file with 65 additions and 0 deletions.
Expand Up @@ -10,6 +10,8 @@
import static com.facebook.react.uimanager.UIManagerHelper.getReactContext;
import static com.facebook.react.views.text.TextAttributeProps.UNSET;

import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.graphics.Color;
import android.graphics.Paint;
Expand All @@ -28,8 +30,11 @@
import android.text.method.KeyListener;
import android.text.method.QwertyKeyListener;
import android.util.TypedValue;
import android.view.ActionMode;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.accessibility.AccessibilityNodeInfo;
Expand Down Expand Up @@ -181,6 +186,35 @@ public boolean performAccessibilityAction(View host, int action, Bundle args) {
}
};
ViewCompat.setAccessibilityDelegate(this, editTextAccessibilityDelegate);
ActionMode.Callback customActionModeCallback =
new ActionMode.Callback() {
/*
* Editor onCreateActionMode adds the cut, copy, paste, share, autofill,
* and paste as plain text items to the context menu.
*/
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
menu.removeItem(android.R.id.pasteAsPlainText);
return true;
}

@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return true;
}

@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
return false;
}

@Override
public void onDestroyActionMode(ActionMode mode) {}
};
setCustomSelectionActionModeCallback(customActionModeCallback);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
setCustomInsertionActionModeCallback(customActionModeCallback);
}
}

@Override
Expand Down Expand Up @@ -269,6 +303,37 @@ public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
return inputConnection;
}

/*
* Called when a context menu option for the text view is selected.
* React Native replaces copy (as rich text) with copy as plain text.
*/
@Override
public boolean onTextContextMenuItem(int id) {
if (id == android.R.id.paste) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
id = android.R.id.pasteAsPlainText;
} else {
ClipboardManager clipboard =
(ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
ClipData previousClipData = clipboard.getPrimaryClip();
if (previousClipData != null) {
for (int i = 0; i < previousClipData.getItemCount(); i++) {
final CharSequence text = previousClipData.getItemAt(i).coerceToText(getContext());
final CharSequence paste = (text instanceof Spanned) ? text.toString() : text;
if (paste != null) {
ClipData clipData = ClipData.newPlainText(null, text);
clipboard.setPrimaryClip(clipData);
}
}
boolean actionPerformed = super.onTextContextMenuItem(id);
clipboard.setPrimaryClip(previousClipData);
return actionPerformed;
}
}
}
return super.onTextContextMenuItem(id);
}

@Override
public void clearFocus() {
setFocusableInTouchMode(false);
Expand Down

0 comments on commit b1ceea4

Please sign in to comment.