diff --git a/packages/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js b/packages/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js index 55b770d26a35ef..f5d06e24595213 100644 --- a/packages/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js +++ b/packages/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js @@ -485,6 +485,11 @@ export type NativeProps = $ReadOnly<{| */ selectionColor?: ?ColorValue, + /** + * The text selection handle color. + */ + selectionHandleColor?: ?ColorValue, + /** * The start and end of the text input's selection. Set start and end to * the same value to position the cursor. @@ -692,6 +697,9 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = { fontStyle: true, textShadowOffset: true, selectionColor: {process: require('../../StyleSheet/processColor').default}, + selectionHandleColor: { + process: require('../../StyleSheet/processColor').default, + }, placeholderTextColor: { process: require('../../StyleSheet/processColor').default, }, diff --git a/packages/react-native/Libraries/Components/TextInput/TextInput.d.ts b/packages/react-native/Libraries/Components/TextInput/TextInput.d.ts index 80db8f0a652225..7234cbb3c325c5 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.d.ts +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.d.ts @@ -336,6 +336,14 @@ export interface TextInputAndroidProps { */ cursorColor?: ColorValue | null | undefined; + /** + * When provided it will set the color of the selection handles when highlighting text. + * Unlike the behavior of `selectionColor` the handle color will be set independently + * from the color of the text selection box. + * @platform android + */ + selectionHandleColor?: ColorValue | null | undefined; + /** * Determines whether the individual fields in your app should be included in a * view structure for autofill purposes on Android API Level 26+. Defaults to auto. diff --git a/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js b/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js index 0eb8f578d65879..638acd7c7925a2 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.flow.js @@ -332,6 +332,14 @@ type AndroidProps = $ReadOnly<{| */ cursorColor?: ?ColorValue, + /** + * When provided it will set the color of the selection handles when highlighting text. + * Unlike the behavior of `selectionColor` the handle color will be set independently + * from the color of the text selection box. + * @platform android + */ + selectionHandleColor?: ?ColorValue, + /** * When `false`, if there is a small amount of space available around a text input * (e.g. landscape orientation on a phone), the OS may choose to have the user edit diff --git a/packages/react-native/Libraries/Components/TextInput/TextInput.js b/packages/react-native/Libraries/Components/TextInput/TextInput.js index 3e0d8bf7681dd3..0162f2bb007b02 100644 --- a/packages/react-native/Libraries/Components/TextInput/TextInput.js +++ b/packages/react-native/Libraries/Components/TextInput/TextInput.js @@ -917,6 +917,12 @@ export type Props = $ReadOnly<{| */ selectionColor?: ?ColorValue, + /** + * The text selection handle color. + * @platform android + */ + selectionHandleColor?: ?ColorValue, + /** * If `true`, all text will automatically be selected on focus. */ @@ -1111,6 +1117,9 @@ function InternalTextInput(props: Props): React.Node { id, tabIndex, selection: propsSelection, + selectionColor, + selectionHandleColor, + cursorColor, ...otherProps } = props; @@ -1506,7 +1515,15 @@ function InternalTextInput(props: Props): React.Node { if (childCount > 1) { children = {children}; } - + // For consistency with iOS set cursor/selectionHandle color as selectionColor + const colorProps = { + selectionColor, + selectionHandleColor: + selectionHandleColor === undefined + ? selectionColor + : selectionHandleColor, + cursorColor: cursorColor === undefined ? selectionColor : cursorColor, + }; textInput = ( /* $FlowFixMe[prop-missing] the types for AndroidTextInput don't match up * exactly with the props for TextInput. This will need to get fixed */ @@ -1520,6 +1537,7 @@ function InternalTextInput(props: Props): React.Node { // $FlowFixMe[incompatible-type] - Figure out imperative + forward refs. ref={ref} {...otherProps} + {...colorProps} {...eventHandlers} accessibilityState={_accessibilityState} accessibilityLabelledBy={_accessibilityLabelledBy} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java index 8496a7d059e4c8..0816eeb102e4f4 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java @@ -168,14 +168,11 @@ public class ReactTextInputManager extends BaseViewManager= Build.VERSION_CODES.Q) { - Drawable cursorDrawable = view.getTextCursorDrawable(); - if (cursorDrawable != null) { - cursorDrawable.setColorFilter(new BlendModeColorFilter(color, BlendMode.SRC_IN)); - view.setTextCursorDrawable(cursorDrawable); + Drawable drawableCenter = view.getTextSelectHandle().mutate(); + Drawable drawableLeft = view.getTextSelectHandleLeft().mutate(); + Drawable drawableRight = view.getTextSelectHandleRight().mutate(); + if (color != null) { + BlendModeColorFilter filter = new BlendModeColorFilter(color, BlendMode.SRC_IN); + drawableCenter.setColorFilter(filter); + drawableLeft.setColorFilter(filter); + drawableRight.setColorFilter(filter); + } else { + drawableCenter.clearColorFilter(); + drawableLeft.clearColorFilter(); + drawableRight.clearColorFilter(); } + view.setTextSelectHandle(drawableCenter); + view.setTextSelectHandleLeft(drawableLeft); + view.setTextSelectHandleRight(drawableRight); return; } + // Based on https://github.com/facebook/react-native/pull/31007 if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) { - // Pre-Android 10, there was no supported API to change the cursor color programmatically. - // In Android 9.0, they changed the underlying implementation, - // but also "dark greylisted" the new field, rendering it unusable. return; } - // The evil code that follows uses reflection to achieve this on Android 8.1 and below. - // Based on https://tinyurl.com/3vff8lyu https://tinyurl.com/vehggzs9 - for (int i = 0; i < DRAWABLE_RESOURCES.length; i++) { + // The following code uses reflection to change handles color on Android 8.1 and below. + for (int i = 0; i < DRAWABLE_HANDLE_RESOURCES.length; i++) { try { - Field drawableResourceField = TextView.class.getDeclaredField(DRAWABLE_RESOURCES[i]); + Field drawableResourceField = + view.getClass().getDeclaredField(DRAWABLE_HANDLE_RESOURCES[i]); drawableResourceField.setAccessible(true); int resourceId = drawableResourceField.getInt(view); - // The view has no cursor drawable. + // The view has no handle drawable. if (resourceId == 0) { return; } - Drawable drawable = ContextCompat.getDrawable(view.getContext(), resourceId); - - Drawable drawableCopy = drawable.mutate(); - drawableCopy.setColorFilter(color, PorterDuff.Mode.SRC_IN); + Drawable drawable = ContextCompat.getDrawable(view.getContext(), resourceId).mutate(); + if (color != null) { + drawable.setColorFilter(color, PorterDuff.Mode.SRC_IN); + } else { + drawable.clearColorFilter(); + } Field editorField = TextView.class.getDeclaredField("mEditor"); editorField.setAccessible(true); Object editor = editorField.get(view); - Field cursorDrawableField = editor.getClass().getDeclaredField(DRAWABLE_FIELDS[i]); + Field cursorDrawableField = editor.getClass().getDeclaredField(DRAWABLE_HANDLE_FIELDS[i]); cursorDrawableField.setAccessible(true); - if (DRAWABLE_RESOURCES[i] == "mCursorDrawableRes") { - Drawable[] drawables = {drawableCopy, drawableCopy}; - cursorDrawableField.set(editor, drawables); - } else { - cursorDrawableField.set(editor, drawableCopy); - } + cursorDrawableField.set(editor, drawable); } catch (NoSuchFieldException ex) { - // Ignore errors to avoid crashing if these private fields don't exist on modified - // or future android versions. } catch (IllegalAccessException ex) { } } } + @ReactProp(name = "cursorColor", customType = "Color") + public void setCursorColor(ReactEditText view, @Nullable Integer color) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + Drawable cursorDrawable = view.getTextCursorDrawable(); + if (cursorDrawable != null) { + if (color != null) { + cursorDrawable.setColorFilter(new BlendModeColorFilter(color, BlendMode.SRC_IN)); + } else { + cursorDrawable.clearColorFilter(); + } + view.setTextCursorDrawable(cursorDrawable); + } + return; + } + + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) { + // Pre-Android 10, there was no supported API to change the cursor color programmatically. + // In Android 9.0, they changed the underlying implementation, + // but also "dark greylisted" the new field, rendering it unusable. + return; + } + + // The evil code that follows uses reflection to achieve this on Android 8.1 and below. + // Based on https://tinyurl.com/3vff8lyu https://tinyurl.com/vehggzs9 + try { + Field drawableCursorField = view.getClass().getDeclaredField("mCursorDrawableRes"); + drawableCursorField.setAccessible(true); + int resourceId = drawableCursorField.getInt(view); + + // The view has no cursor drawable. + if (resourceId == 0) { + return; + } + + Drawable drawable = ContextCompat.getDrawable(view.getContext(), resourceId).mutate(); + if (color != null) { + drawable.setColorFilter(color, PorterDuff.Mode.SRC_IN); + } else { + drawable.clearColorFilter(); + } + + Field editorField = TextView.class.getDeclaredField("mEditor"); + editorField.setAccessible(true); + Object editor = editorField.get(view); + + Field cursorDrawableField = editor.getClass().getDeclaredField("mCursorDrawable"); + cursorDrawableField.setAccessible(true); + Drawable[] drawables = {drawable, drawable}; + cursorDrawableField.set(editor, drawables); + } catch (NoSuchFieldException ex) { + // Ignore errors to avoid crashing if these private fields don't exist on modified + // or future android versions. + } catch (IllegalAccessException ex) { + } + } + private static boolean shouldHideCursorForEmailTextInput() { String manufacturer = Build.MANUFACTURER.toLowerCase(Locale.ROOT); return (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q && manufacturer.contains("xiaomi")); diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp index 116284f1c6bcaa..2687d64cb3f86f 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp @@ -134,6 +134,10 @@ AndroidTextInputProps::AndroidTextInputProps( "selectionColor", sourceProps.selectionColor, {})), + selectionHandleColor(CoreFeatures::enablePropIteratorSetter? sourceProps.selectionHandleColor : convertRawProp(context, rawProps, + "selectionHandleColor", + sourceProps.selectionHandleColor, + {})), value(CoreFeatures::enablePropIteratorSetter? sourceProps.value : convertRawProp(context, rawProps, "value", sourceProps.value, {})), defaultValue(CoreFeatures::enablePropIteratorSetter? sourceProps.defaultValue : convertRawProp(context, rawProps, "defaultValue", @@ -347,6 +351,7 @@ void AndroidTextInputProps::setProp( RAW_SET_PROP_SWITCH_CASE_BASIC(placeholderTextColor); RAW_SET_PROP_SWITCH_CASE_BASIC(secureTextEntry); RAW_SET_PROP_SWITCH_CASE_BASIC(selectionColor); + RAW_SET_PROP_SWITCH_CASE_BASIC(selectionHandleColor); RAW_SET_PROP_SWITCH_CASE_BASIC(defaultValue); RAW_SET_PROP_SWITCH_CASE_BASIC(selectTextOnFocus); RAW_SET_PROP_SWITCH_CASE_BASIC(submitBehavior); @@ -446,6 +451,7 @@ folly::dynamic AndroidTextInputProps::getDynamic() const { props["placeholderTextColor"] = toAndroidRepr(placeholderTextColor); props["secureTextEntry"] = secureTextEntry; props["selectionColor"] = toAndroidRepr(selectionColor); + props["selectionHandleColor"] = toAndroidRepr(selectionHandleColor); props["value"] = value; props["defaultValue"] = defaultValue; props["selectTextOnFocus"] = selectTextOnFocus; diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.h b/packages/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.h index a690816906ae2f..82fb75ea1028e4 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.h +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.h @@ -100,6 +100,7 @@ class AndroidTextInputProps final : public ViewProps, public BaseTextProps { SharedColor placeholderTextColor{}; bool secureTextEntry{false}; SharedColor selectionColor{}; + SharedColor selectionHandleColor{}; std::string value{}; std::string defaultValue{}; bool selectTextOnFocus{false}; diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputProps.cpp index 3e4b712e91d06c..a825971ddbe8c8 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputProps.cpp @@ -62,6 +62,12 @@ TextInputProps::TextInputProps( "selectionColor", sourceProps.selectionColor, {})), + selectionHandleColor(convertRawProp( + context, + rawProps, + "selectionHandleColor", + sourceProps.selectionHandleColor, + {})), underlineColorAndroid(convertRawProp( context, rawProps, diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputProps.h b/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputProps.h index 203f762ede10ab..e3db155099c78e 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputProps.h +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputProps.h @@ -53,6 +53,7 @@ class TextInputProps final : public ViewProps, public BaseTextProps { */ const SharedColor cursorColor{}; const SharedColor selectionColor{}; + const SharedColor selectionHandleColor{}; // TODO: Rename to `tintColor` and make universal. const SharedColor underlineColorAndroid{}; diff --git a/packages/rn-tester/js/examples/TextInput/TextInputExample.android.js b/packages/rn-tester/js/examples/TextInput/TextInputExample.android.js index d0ad866155858d..35347b720cedc6 100644 --- a/packages/rn-tester/js/examples/TextInput/TextInputExample.android.js +++ b/packages/rn-tester/js/examples/TextInput/TextInputExample.android.js @@ -196,10 +196,20 @@ const examples: Array = [ + + ); }, @@ -470,7 +480,7 @@ const examples: Array = [ 'next', ]; const returnKeyLabels = ['Compile', 'React Native']; - const examples = returnKeyTypes.map(type => { + const returnKeyExamples = returnKeyTypes.map(type => { return ( = [ }); return ( - {examples} + {returnKeyExamples} {types} );