Skip to content

Commit

Permalink
Add cursorColor support to TextInput (#11502)
Browse files Browse the repository at this point in the history
## Description

I've added the support for the `cursorColor` property to the macOS repo already. This PR will help to keep parity between platforms microsoft/react-native-macos#1787 

### Type of Change
- New feature (non-breaking change which adds functionality)

### Why
For our needs, we need to have more control over cursor/caret color. The `cusrorColor` property is already available on macOS after microsoft/react-native-macos#1787 and documented in the docs https://reactnative.dev/docs/textinput#cursorcolor-android. However, there is currently no way to update the cursor/caret color on Windows.

### What
- I've extended `HideCaretIfNeeded` logic to support the custom color of a caret. This function is already, in a sense, changing the color of a caret; however, it always sets it to be transparent. Now, we allow custom colors as well
- Updated demo textinput page to have test cases for it

## Screenshots

https://user-images.githubusercontent.com/963490/232445282-96c509e8-801d-4009-bbfd-c061340f53e8.mp4
  • Loading branch information
dlitsman authored Apr 21, 2023
1 parent 06fbd58 commit f9c92c6
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 83 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "Add cursorColor support to TextInput",
"packageName": "react-native-windows",
"email": "[email protected]",
"dependentChangeType": "patch"
}
200 changes: 125 additions & 75 deletions packages/playground/Samples/textinput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
Keyboard,
View,
KeyboardAvoidingView,
ScrollView,
} from 'react-native';

import type {EventSubscription} from 'react-native/Libraries/vendor/emitter/EventEmitter';
Expand Down Expand Up @@ -58,84 +59,133 @@ export default class Bootstrap extends React.Component<{}, any> {
render() {
let textInputRef: TextInput | null;
return (
<View style={styles.container}>
<TextInput
style={styles.input}
placeholder={'MultiLine'}
multiline={true}
/>
<TextInput
style={styles.input}
placeholder={'ReadOnly'}
editable={false}
/>
<TextInput
style={styles.input}
placeholder={'SpellChecking Disabled'}
spellCheck={false}
/>
<TextInput
style={styles.input}
placeholder={'PlaceHolder color blue'}
placeholderTextColor="blue"
/>
<TextInput
style={styles.input}
placeholder={'contextMenuHidden'}
contextMenuHidden={true}
/>
<TextInput
style={styles.input}
caretHidden={true}
placeholder={'caretHidden'}
/>
<TextInput
style={styles.input}
keyboardType="number-pad"
placeholder={'number-pad keyboardType'}
/>
<TextInput
style={styles.input}
autoCapitalize="characters"
placeholder={'autoCapitalize characters'}
/>
<TextInput
ref={ref => (textInputRef = ref)}
onFocus={() => setTimeout(() => textInputRef?.blur(), 5000)}
placeholder={'blurs after 5 seconds'}
style={styles.input}
/>
<TextInput
style={styles.input}
placeholder={this.state.passwordHidden ? 'Password' : 'Text'}
autoCapitalize="none"
secureTextEntry={this.state.passwordHidden}
onChangeText={text => {
this.setState({text});
}}
value={this.state.text}
selectionColor="red"
maxLength={10}
keyboardType="numeric"
/>
<Button
title={
this.state.passwordHidden
? 'SecureTextEntry On'
: 'SecureTextEntry Off'
}
onPress={this.onPressShowPassword}
/>
<KeyboardAvoidingView
style={styles.container}
behavior="padding"
enabled>
<ScrollView>
<View style={styles.container}>
<TextInput
style={styles.input}
placeholder={'KeyboardAvoidingView padding'}
placeholder={'MultiLine'}
multiline={true}
/>
</KeyboardAvoidingView>
</View>
<TextInput
style={styles.input}
placeholder={'ReadOnly'}
editable={false}
/>
<TextInput
style={styles.input}
placeholder={'SpellChecking Disabled'}
spellCheck={false}
/>
<TextInput
style={styles.input}
placeholder={'PlaceHolder color blue'}
placeholderTextColor="blue"
/>
<TextInput
style={styles.input}
placeholder={'contextMenuHidden'}
contextMenuHidden={true}
/>
<TextInput
style={styles.input}
caretHidden={true}
placeholder={'caretHidden'}
/>
<TextInput
style={styles.input}
keyboardType="number-pad"
placeholder={'number-pad keyboardType'}
/>
<TextInput
style={styles.input}
autoCapitalize="characters"
placeholder={'autoCapitalize characters'}
/>
<TextInput
ref={ref => (textInputRef = ref)}
onFocus={() => setTimeout(() => textInputRef?.blur(), 5000)}
placeholder={'blurs after 5 seconds'}
style={styles.input}
/>
<TextInput
style={styles.input}
placeholder={this.state.passwordHidden ? 'Password' : 'Text'}
autoCapitalize="none"
secureTextEntry={this.state.passwordHidden}
onChangeText={text => {
this.setState({text});
}}
value={this.state.text}
selectionColor="red"
maxLength={10}
keyboardType="numeric"
/>
<Button
title={
this.state.passwordHidden
? 'SecureTextEntry On'
: 'SecureTextEntry Off'
}
onPress={this.onPressShowPassword}
/>
<TextInput
placeholder="Single line"
cursorColor="#00FF00"
placeholderTextColor="grey"
style={[
styles.input,
{backgroundColor: 'black', color: 'white', marginBottom: 4},
]}
/>
<TextInput
placeholder="Single line with caret color and caret hidden"
cursorColor="#00FF00"
caretHidden={true}
placeholderTextColor="grey"
style={[
styles.input,
{backgroundColor: 'black', color: 'white', marginBottom: 4},
]}
/>
<TextInput
multiline={true}
placeholder="Multiline"
cursorColor="#00FF00"
placeholderTextColor="grey"
style={[
styles.input,
{backgroundColor: 'black', color: 'white', marginBottom: 4},
]}
/>
<TextInput
placeholder="Single line with selection color"
cursorColor="#00FF00"
selectionColor="yellow"
placeholderTextColor="grey"
style={[
styles.input,
{backgroundColor: 'black', color: 'white', marginBottom: 4},
]}
/>
<TextInput
multiline={true}
placeholder="Multiline with selection color"
cursorColor="#00FF00"
selectionColor="yellow"
placeholderTextColor="grey"
style={[styles.input, {backgroundColor: 'black', color: 'white'}]}
/>
<KeyboardAvoidingView
style={styles.container}
behavior="padding"
enabled>
<TextInput
style={styles.input}
placeholder={'KeyboardAvoidingView padding'}
/>
</KeyboardAvoidingView>
</View>
</ScrollView>
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ WindowsTextInputProps::WindowsTextInputProps(
placeholderTextColor(
convertRawProp(context, rawProps, "placeholderTextColor", sourceProps.placeholderTextColor, {})),
scrollEnabled(convertRawProp(context, rawProps, "scrollEnabled", sourceProps.scrollEnabled, {true})),
cursorColor(convertRawProp(context, rawProps, "cursorColor", sourceProps.cursorColor, {})),
selection(convertRawProp(context, rawProps, "selection", sourceProps.selection, {})),
selectionColor(convertRawProp(context, rawProps, "selectionColor", sourceProps.selectionColor, {})),
selectTextOnFocus(convertRawProp(context, rawProps, "selectTextOnFocus", sourceProps.selectTextOnFocus, {false})),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ class WindowsTextInputProps final : public ViewProps, public BaseTextProps {
std::string placeholder{};
SharedColor placeholderTextColor{};
bool scrollEnabled{true};
SharedColor cursorColor{};
CompWindowsTextInputSelectionStruct selection{};
SharedColor selectionColor{};
bool selectTextOnFocus{false};
Expand Down
34 changes: 26 additions & 8 deletions vnext/Microsoft.ReactNative/Views/TextInputViewManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ class TextInputShadowNode : public ShadowNodeBase {
void dispatchTextInputChangeEvent(winrt::hstring newText);
void registerEvents();
void registerPreviewKeyDown();
void HideCaretIfNeeded();
void UpdateCaretColorOrHideIfNeeded();
void setPasswordBoxPlaceholderForeground(
xaml::Controls::PasswordBox passwordBox,
const winrt::Microsoft::ReactNative::JSValue &color);
Expand All @@ -144,6 +144,8 @@ class TextInputShadowNode : public ShadowNodeBase {
bool m_hideCaret = false;
bool m_shouldClearTextOnSubmit = false;

winrt::Microsoft::ReactNative::JSValue m_cursorColor;

winrt::Microsoft::ReactNative::JSValue m_placeholderTextColor;
std::vector<HandledKeyboardEvent> m_submitKeyEvents{};

Expand Down Expand Up @@ -260,7 +262,7 @@ void TextInputShadowNode::registerEvents() {
control.as<xaml::Controls::PasswordBox>().SelectAll();
}
}
HideCaretIfNeeded();
UpdateCaretColorOrHideIfNeeded();

folly::dynamic eventData = folly::dynamic::object("target", tag);
if (!m_updating)
Expand Down Expand Up @@ -330,7 +332,7 @@ void TextInputShadowNode::registerEvents() {
}
});
}
HideCaretIfNeeded();
UpdateCaretColorOrHideIfNeeded();
});

if (control.try_as<xaml::IUIElement7>()) {
Expand Down Expand Up @@ -448,14 +450,24 @@ bool TextInputShadowNode::IsTextBox() {
return !!GetView().try_as<xaml::Controls::TextBox>();
}

// hacking solution to hide the caret
void TextInputShadowNode::HideCaretIfNeeded() {
// hacking solution to hide the caret or change its color
void TextInputShadowNode::UpdateCaretColorOrHideIfNeeded() {
bool updateRequired = false;
xaml::Media::SolidColorBrush color;

if (m_hideCaret) {
updateRequired = true;
color = xaml::Media::SolidColorBrush(winrt::Colors::Transparent());
} else if (!m_cursorColor.IsNull()) {
updateRequired = true;
color = SolidColorBrushFrom(m_cursorColor);
}

if (updateRequired) {
auto control = GetView().as<xaml::Controls::Control>();
if (auto caret = FindCaret(control)) {
caret.CompositeMode(xaml::Media::ElementCompositeMode::Inherit);
xaml::Media::SolidColorBrush transparentColor(winrt::Colors::Transparent());
caret.Fill(transparentColor);
caret.Fill(color);
}
}
}
Expand Down Expand Up @@ -520,7 +532,7 @@ void TextInputShadowNode::updateProperties(winrt::Microsoft::ReactNative::JSValu
} else if (propertyName == "caretHidden") {
if (propertyValue.Type() == winrt::Microsoft::ReactNative::JSValueType::Boolean) {
m_hideCaret = propertyValue.AsBoolean();
HideCaretIfNeeded();
UpdateCaretColorOrHideIfNeeded();
}
} else if (propertyName == "focusable") {
// parent class also sets isTabStop
Expand Down Expand Up @@ -581,6 +593,11 @@ void TextInputShadowNode::updateProperties(winrt::Microsoft::ReactNative::JSValu
isTextBox ? xaml::Controls::TextBox::PlaceholderTextProperty()
: xaml::Controls::PasswordBox::PlaceholderTextProperty());
}
} else if (propertyName == "cursorColor") {
m_cursorColor = nullptr;
if (IsValidColorValue(propertyValue)) {
m_cursorColor = propertyValue.Copy();
}
} else if (propertyName == "selectionColor") {
if (IsValidColorValue(propertyValue)) {
control.SetValue(
Expand Down Expand Up @@ -822,6 +839,7 @@ void TextInputViewManager::GetNativeProps(const winrt::Microsoft::ReactNative::I
React::WriteProperty(writer, L"placeholder", L"string");
React::WriteProperty(writer, L"placeholderTextColor", L"Color");
React::WriteProperty(writer, L"scrollEnabled", L"boolean");
React::WriteProperty(writer, L"cursorColor", L"Color");
React::WriteProperty(writer, L"selection", L"Map");
React::WriteProperty(writer, L"selectionColor", L"Color");
React::WriteProperty(writer, L"selectTextOnFocus", L"boolean");
Expand Down

0 comments on commit f9c92c6

Please sign in to comment.