Skip to content

Commit 1e68e48

Browse files
jakex7facebook-github-bot
authored andcommitted
feat: selectionHandleColor prop on Android (#41092)
Summary: This PR addresses the problem raised in the #41004 issue. The current logic is that `selectionColor` on iOS sets the color of the selection, handles, and cursor. On Android it looks similar, while it doesn't change the color of the handles if the API level is higher than 27. In addition, on Android there was an option to set the color of the cursor by `cursorColor` prop, but it didn't work if the `selectionCursor` was set. ## Changelog: <!-- Help reviewers and the release process by writing your own changelog entry. Pick one each for the category and type tags: [ANDROID|GENERAL|IOS|INTERNAL] [BREAKING|ADDED|CHANGED|DEPRECATED|REMOVED|FIXED|SECURITY] - Message For more details, see: https://reactnative.dev/contributing/changelogs-in-pull-requests --> [GENERAL] [ADDED] - Make same behavior of the `selectionColor` prop on Android as iOS [ANDROID] [ADDED] - Introduced `selectionHandleColor` as a separate prop [ANDROID] [CHANGED] - Allowing `cursorColor` and `selectionHandleColor` to override `selectionColor` on Android Pull Request resolved: #41092 Test Plan: Manual tests in rn-tester: ### `selectionColor` same as iOS, sets selection, handles and cursor color _There is a way to set only "rectangle" color by setting other props as null_ ![image](https://github.com/facebook/react-native/assets/39670088/9cba34c2-c9fc-4d84-a9cb-3b28a754671d) ### `selectionHandleColor` ![image](https://github.com/facebook/react-native/assets/39670088/8a7e488e-0e35-4646-9efe-4783420b41fa) ### `cursorColor` ![image](https://github.com/facebook/react-native/assets/39670088/06798b8a-851f-44c7-979e-a4e74681b29a) Reviewed By: NickGerleman Differential Revision: D51253298 Pulled By: javache fbshipit-source-id: 290284aa38c6ba0aa6998b937258788ce6376431
1 parent 6b89dc1 commit 1e68e48

File tree

10 files changed

+161
-44
lines changed

10 files changed

+161
-44
lines changed

packages/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,11 @@ export type NativeProps = $ReadOnly<{|
485485
*/
486486
selectionColor?: ?ColorValue,
487487

488+
/**
489+
* The text selection handle color.
490+
*/
491+
selectionHandleColor?: ?ColorValue,
492+
488493
/**
489494
* The start and end of the text input's selection. Set start and end to
490495
* the same value to position the cursor.
@@ -692,6 +697,9 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = {
692697
fontStyle: true,
693698
textShadowOffset: true,
694699
selectionColor: {process: require('../../StyleSheet/processColor').default},
700+
selectionHandleColor: {
701+
process: require('../../StyleSheet/processColor').default,
702+
},
695703
placeholderTextColor: {
696704
process: require('../../StyleSheet/processColor').default,
697705
},

packages/react-native/Libraries/Components/TextInput/TextInput.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,14 @@ export interface TextInputAndroidProps {
336336
*/
337337
cursorColor?: ColorValue | null | undefined;
338338

339+
/**
340+
* When provided it will set the color of the selection handles when highlighting text.
341+
* Unlike the behavior of `selectionColor` the handle color will be set independently
342+
* from the color of the text selection box.
343+
* @platform android
344+
*/
345+
selectionHandleColor?: ColorValue | null | undefined;
346+
339347
/**
340348
* Determines whether the individual fields in your app should be included in a
341349
* view structure for autofill purposes on Android API Level 26+. Defaults to auto.

packages/react-native/Libraries/Components/TextInput/TextInput.flow.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,14 @@ type AndroidProps = $ReadOnly<{|
332332
*/
333333
cursorColor?: ?ColorValue,
334334

335+
/**
336+
* When provided it will set the color of the selection handles when highlighting text.
337+
* Unlike the behavior of `selectionColor` the handle color will be set independently
338+
* from the color of the text selection box.
339+
* @platform android
340+
*/
341+
selectionHandleColor?: ?ColorValue,
342+
335343
/**
336344
* When `false`, if there is a small amount of space available around a text input
337345
* (e.g. landscape orientation on a phone), the OS may choose to have the user edit

packages/react-native/Libraries/Components/TextInput/TextInput.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -917,6 +917,12 @@ export type Props = $ReadOnly<{|
917917
*/
918918
selectionColor?: ?ColorValue,
919919

920+
/**
921+
* The text selection handle color.
922+
* @platform android
923+
*/
924+
selectionHandleColor?: ?ColorValue,
925+
920926
/**
921927
* If `true`, all text will automatically be selected on focus.
922928
*/
@@ -1111,6 +1117,9 @@ function InternalTextInput(props: Props): React.Node {
11111117
id,
11121118
tabIndex,
11131119
selection: propsSelection,
1120+
selectionColor,
1121+
selectionHandleColor,
1122+
cursorColor,
11141123
...otherProps
11151124
} = props;
11161125

@@ -1506,7 +1515,15 @@ function InternalTextInput(props: Props): React.Node {
15061515
if (childCount > 1) {
15071516
children = <Text>{children}</Text>;
15081517
}
1509-
1518+
// For consistency with iOS set cursor/selectionHandle color as selectionColor
1519+
const colorProps = {
1520+
selectionColor,
1521+
selectionHandleColor:
1522+
selectionHandleColor === undefined
1523+
? selectionColor
1524+
: selectionHandleColor,
1525+
cursorColor: cursorColor === undefined ? selectionColor : cursorColor,
1526+
};
15101527
textInput = (
15111528
/* $FlowFixMe[prop-missing] the types for AndroidTextInput don't match up
15121529
* exactly with the props for TextInput. This will need to get fixed */
@@ -1520,6 +1537,7 @@ function InternalTextInput(props: Props): React.Node {
15201537
// $FlowFixMe[incompatible-type] - Figure out imperative + forward refs.
15211538
ref={ref}
15221539
{...otherProps}
1540+
{...colorProps}
15231541
{...eventHandlers}
15241542
accessibilityState={_accessibilityState}
15251543
accessibilityLabelledBy={_accessibilityLabelledBy}

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java

Lines changed: 91 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -168,14 +168,11 @@ public class ReactTextInputManager extends BaseViewManager<ReactEditText, Layout
168168
private static final String KEYBOARD_TYPE_URI = "url";
169169
private static final InputFilter[] EMPTY_FILTERS = new InputFilter[0];
170170
private static final int UNSET = -1;
171-
private static final String[] DRAWABLE_FIELDS = {
172-
"mCursorDrawable", "mSelectHandleLeft", "mSelectHandleRight", "mSelectHandleCenter"
171+
private static final String[] DRAWABLE_HANDLE_RESOURCES = {
172+
"mTextSelectHandleLeftRes", "mTextSelectHandleRightRes", "mTextSelectHandleRes"
173173
};
174-
private static final String[] DRAWABLE_RESOURCES = {
175-
"mCursorDrawableRes",
176-
"mTextSelectHandleLeftRes",
177-
"mTextSelectHandleRightRes",
178-
"mTextSelectHandleRes"
174+
private static final String[] DRAWABLE_HANDLE_FIELDS = {
175+
"mSelectHandleLeft", "mSelectHandleRight", "mSelectHandleCenter"
179176
};
180177

181178
protected @Nullable ReactTextViewManagerCallback mReactTextViewManagerCallback;
@@ -524,70 +521,124 @@ public void setSelectionColor(ReactEditText view, @Nullable Integer color) {
524521
} else {
525522
view.setHighlightColor(color);
526523
}
527-
528-
setCursorColor(view, color);
529524
}
530525

531-
@ReactProp(name = "cursorColor", customType = "Color")
532-
public void setCursorColor(ReactEditText view, @Nullable Integer color) {
533-
if (color == null) {
534-
return;
535-
}
536-
526+
@ReactProp(name = "selectionHandleColor", customType = "Color")
527+
public void setSelectionHandleColor(ReactEditText view, @Nullable Integer color) {
537528
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
538-
Drawable cursorDrawable = view.getTextCursorDrawable();
539-
if (cursorDrawable != null) {
540-
cursorDrawable.setColorFilter(new BlendModeColorFilter(color, BlendMode.SRC_IN));
541-
view.setTextCursorDrawable(cursorDrawable);
529+
Drawable drawableCenter = view.getTextSelectHandle().mutate();
530+
Drawable drawableLeft = view.getTextSelectHandleLeft().mutate();
531+
Drawable drawableRight = view.getTextSelectHandleRight().mutate();
532+
if (color != null) {
533+
BlendModeColorFilter filter = new BlendModeColorFilter(color, BlendMode.SRC_IN);
534+
drawableCenter.setColorFilter(filter);
535+
drawableLeft.setColorFilter(filter);
536+
drawableRight.setColorFilter(filter);
537+
} else {
538+
drawableCenter.clearColorFilter();
539+
drawableLeft.clearColorFilter();
540+
drawableRight.clearColorFilter();
542541
}
542+
view.setTextSelectHandle(drawableCenter);
543+
view.setTextSelectHandleLeft(drawableLeft);
544+
view.setTextSelectHandleRight(drawableRight);
543545
return;
544546
}
545547

548+
// Based on https://github.com/facebook/react-native/pull/31007
546549
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) {
547-
// Pre-Android 10, there was no supported API to change the cursor color programmatically.
548-
// In Android 9.0, they changed the underlying implementation,
549-
// but also "dark greylisted" the new field, rendering it unusable.
550550
return;
551551
}
552552

553-
// The evil code that follows uses reflection to achieve this on Android 8.1 and below.
554-
// Based on https://tinyurl.com/3vff8lyu https://tinyurl.com/vehggzs9
555-
for (int i = 0; i < DRAWABLE_RESOURCES.length; i++) {
553+
// The following code uses reflection to change handles color on Android 8.1 and below.
554+
for (int i = 0; i < DRAWABLE_HANDLE_RESOURCES.length; i++) {
556555
try {
557-
Field drawableResourceField = TextView.class.getDeclaredField(DRAWABLE_RESOURCES[i]);
556+
Field drawableResourceField =
557+
view.getClass().getDeclaredField(DRAWABLE_HANDLE_RESOURCES[i]);
558558
drawableResourceField.setAccessible(true);
559559
int resourceId = drawableResourceField.getInt(view);
560560

561-
// The view has no cursor drawable.
561+
// The view has no handle drawable.
562562
if (resourceId == 0) {
563563
return;
564564
}
565565

566-
Drawable drawable = ContextCompat.getDrawable(view.getContext(), resourceId);
567-
568-
Drawable drawableCopy = drawable.mutate();
569-
drawableCopy.setColorFilter(color, PorterDuff.Mode.SRC_IN);
566+
Drawable drawable = ContextCompat.getDrawable(view.getContext(), resourceId).mutate();
567+
if (color != null) {
568+
drawable.setColorFilter(color, PorterDuff.Mode.SRC_IN);
569+
} else {
570+
drawable.clearColorFilter();
571+
}
570572

571573
Field editorField = TextView.class.getDeclaredField("mEditor");
572574
editorField.setAccessible(true);
573575
Object editor = editorField.get(view);
574576

575-
Field cursorDrawableField = editor.getClass().getDeclaredField(DRAWABLE_FIELDS[i]);
577+
Field cursorDrawableField = editor.getClass().getDeclaredField(DRAWABLE_HANDLE_FIELDS[i]);
576578
cursorDrawableField.setAccessible(true);
577-
if (DRAWABLE_RESOURCES[i] == "mCursorDrawableRes") {
578-
Drawable[] drawables = {drawableCopy, drawableCopy};
579-
cursorDrawableField.set(editor, drawables);
580-
} else {
581-
cursorDrawableField.set(editor, drawableCopy);
582-
}
579+
cursorDrawableField.set(editor, drawable);
583580
} catch (NoSuchFieldException ex) {
584-
// Ignore errors to avoid crashing if these private fields don't exist on modified
585-
// or future android versions.
586581
} catch (IllegalAccessException ex) {
587582
}
588583
}
589584
}
590585

586+
@ReactProp(name = "cursorColor", customType = "Color")
587+
public void setCursorColor(ReactEditText view, @Nullable Integer color) {
588+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
589+
Drawable cursorDrawable = view.getTextCursorDrawable();
590+
if (cursorDrawable != null) {
591+
if (color != null) {
592+
cursorDrawable.setColorFilter(new BlendModeColorFilter(color, BlendMode.SRC_IN));
593+
} else {
594+
cursorDrawable.clearColorFilter();
595+
}
596+
view.setTextCursorDrawable(cursorDrawable);
597+
}
598+
return;
599+
}
600+
601+
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) {
602+
// Pre-Android 10, there was no supported API to change the cursor color programmatically.
603+
// In Android 9.0, they changed the underlying implementation,
604+
// but also "dark greylisted" the new field, rendering it unusable.
605+
return;
606+
}
607+
608+
// The evil code that follows uses reflection to achieve this on Android 8.1 and below.
609+
// Based on https://tinyurl.com/3vff8lyu https://tinyurl.com/vehggzs9
610+
try {
611+
Field drawableCursorField = view.getClass().getDeclaredField("mCursorDrawableRes");
612+
drawableCursorField.setAccessible(true);
613+
int resourceId = drawableCursorField.getInt(view);
614+
615+
// The view has no cursor drawable.
616+
if (resourceId == 0) {
617+
return;
618+
}
619+
620+
Drawable drawable = ContextCompat.getDrawable(view.getContext(), resourceId).mutate();
621+
if (color != null) {
622+
drawable.setColorFilter(color, PorterDuff.Mode.SRC_IN);
623+
} else {
624+
drawable.clearColorFilter();
625+
}
626+
627+
Field editorField = TextView.class.getDeclaredField("mEditor");
628+
editorField.setAccessible(true);
629+
Object editor = editorField.get(view);
630+
631+
Field cursorDrawableField = editor.getClass().getDeclaredField("mCursorDrawable");
632+
cursorDrawableField.setAccessible(true);
633+
Drawable[] drawables = {drawable, drawable};
634+
cursorDrawableField.set(editor, drawables);
635+
} catch (NoSuchFieldException ex) {
636+
// Ignore errors to avoid crashing if these private fields don't exist on modified
637+
// or future android versions.
638+
} catch (IllegalAccessException ex) {
639+
}
640+
}
641+
591642
private static boolean shouldHideCursorForEmailTextInput() {
592643
String manufacturer = Build.MANUFACTURER.toLowerCase(Locale.ROOT);
593644
return (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q && manufacturer.contains("xiaomi"));

packages/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@ AndroidTextInputProps::AndroidTextInputProps(
134134
"selectionColor",
135135
sourceProps.selectionColor,
136136
{})),
137+
selectionHandleColor(CoreFeatures::enablePropIteratorSetter? sourceProps.selectionHandleColor : convertRawProp(context, rawProps,
138+
"selectionHandleColor",
139+
sourceProps.selectionHandleColor,
140+
{})),
137141
value(CoreFeatures::enablePropIteratorSetter? sourceProps.value : convertRawProp(context, rawProps, "value", sourceProps.value, {})),
138142
defaultValue(CoreFeatures::enablePropIteratorSetter? sourceProps.defaultValue : convertRawProp(context, rawProps,
139143
"defaultValue",
@@ -347,6 +351,7 @@ void AndroidTextInputProps::setProp(
347351
RAW_SET_PROP_SWITCH_CASE_BASIC(placeholderTextColor);
348352
RAW_SET_PROP_SWITCH_CASE_BASIC(secureTextEntry);
349353
RAW_SET_PROP_SWITCH_CASE_BASIC(selectionColor);
354+
RAW_SET_PROP_SWITCH_CASE_BASIC(selectionHandleColor);
350355
RAW_SET_PROP_SWITCH_CASE_BASIC(defaultValue);
351356
RAW_SET_PROP_SWITCH_CASE_BASIC(selectTextOnFocus);
352357
RAW_SET_PROP_SWITCH_CASE_BASIC(submitBehavior);
@@ -446,6 +451,7 @@ folly::dynamic AndroidTextInputProps::getDynamic() const {
446451
props["placeholderTextColor"] = toAndroidRepr(placeholderTextColor);
447452
props["secureTextEntry"] = secureTextEntry;
448453
props["selectionColor"] = toAndroidRepr(selectionColor);
454+
props["selectionHandleColor"] = toAndroidRepr(selectionHandleColor);
449455
props["value"] = value;
450456
props["defaultValue"] = defaultValue;
451457
props["selectTextOnFocus"] = selectTextOnFocus;

packages/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ class AndroidTextInputProps final : public ViewProps, public BaseTextProps {
100100
SharedColor placeholderTextColor{};
101101
bool secureTextEntry{false};
102102
SharedColor selectionColor{};
103+
SharedColor selectionHandleColor{};
103104
std::string value{};
104105
std::string defaultValue{};
105106
bool selectTextOnFocus{false};

packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputProps.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ TextInputProps::TextInputProps(
6262
"selectionColor",
6363
sourceProps.selectionColor,
6464
{})),
65+
selectionHandleColor(convertRawProp(
66+
context,
67+
rawProps,
68+
"selectionHandleColor",
69+
sourceProps.selectionHandleColor,
70+
{})),
6571
underlineColorAndroid(convertRawProp(
6672
context,
6773
rawProps,

packages/react-native/ReactCommon/react/renderer/components/textinput/iostextinput/react/renderer/components/iostextinput/TextInputProps.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ class TextInputProps final : public ViewProps, public BaseTextProps {
5353
*/
5454
const SharedColor cursorColor{};
5555
const SharedColor selectionColor{};
56+
const SharedColor selectionHandleColor{};
5657
// TODO: Rename to `tintColor` and make universal.
5758
const SharedColor underlineColorAndroid{};
5859

packages/rn-tester/js/examples/TextInput/TextInputExample.android.js

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -196,10 +196,20 @@ const examples: Array<RNTesterModuleExample> = [
196196
</Text>
197197
</TextInput>
198198
<TextInput
199-
defaultValue="Highlight Color is red"
199+
defaultValue="Selection Color is red"
200200
selectionColor={'red'}
201201
style={styles.singleLine}
202202
/>
203+
<TextInput
204+
defaultValue="Selection handles are red"
205+
selectionHandleColor={'red'}
206+
style={styles.singleLine}
207+
/>
208+
<TextInput
209+
defaultValue="Cursor Color is red"
210+
cursorColor={'red'}
211+
style={styles.singleLine}
212+
/>
203213
</View>
204214
);
205215
},
@@ -470,7 +480,7 @@ const examples: Array<RNTesterModuleExample> = [
470480
'next',
471481
];
472482
const returnKeyLabels = ['Compile', 'React Native'];
473-
const examples = returnKeyTypes.map(type => {
483+
const returnKeyExamples = returnKeyTypes.map(type => {
474484
return (
475485
<TextInput
476486
key={type}
@@ -492,7 +502,7 @@ const examples: Array<RNTesterModuleExample> = [
492502
});
493503
return (
494504
<View>
495-
{examples}
505+
{returnKeyExamples}
496506
{types}
497507
</View>
498508
);

0 commit comments

Comments
 (0)