diff --git a/Libraries/Text/TextInput/RCTBaseTextInputView.m b/Libraries/Text/TextInput/RCTBaseTextInputView.m index d9c47ff16311d4..c7c883d11feb75 100644 --- a/Libraries/Text/TextInput/RCTBaseTextInputView.m +++ b/Libraries/Text/TextInput/RCTBaseTextInputView.m @@ -98,11 +98,43 @@ - (NSAttributedString *)attributedText return self.backedTextInputView.attributedText; } +- (BOOL)textOf:(NSAttributedString*)newText equals:(NSAttributedString*)oldText{ + // When the dictation is running we can't update the attibuted text on the backed up text view + // because setting the attributed string will kill the dictation. This means that we can't impose + // the settings on a dictation. + // Similarly, when the user is in the middle of inputting some text in Japanese/Chinese, there will be styling on the + // text that we should disregard. See https://developer.apple.com/documentation/uikit/uitextinput/1614489-markedtextrange?language=objc + // for more info. + // Lastly, when entering a password, etc., there will be additional styling on the field as the native text view + // handles showing the last character for a split second. + BOOL shouldFallbackToBareTextComparison = + [self.backedTextInputView.textInputMode.primaryLanguage isEqualToString:@"dictation"] || + self.backedTextInputView.markedTextRange || + self.backedTextInputView.isSecureTextEntry; + if (shouldFallbackToBareTextComparison) { + return ([newText.string isEqualToString:oldText.string]); + } else { + return ([newText isEqualToAttributedString:oldText]); + } +} + - (void)setAttributedText:(NSAttributedString *)attributedText { NSInteger eventLag = _nativeEventCount - _mostRecentEventCount; + BOOL textNeedsUpdate = NO; + // Remove tag attribute to ensure correct attributed string comparison. + NSMutableAttributedString *const backedTextInputViewTextCopy = [self.backedTextInputView.attributedText mutableCopy]; + NSMutableAttributedString *const attributedTextCopy = [attributedText mutableCopy]; + + [backedTextInputViewTextCopy removeAttribute:RCTTextAttributesTagAttributeName + range:NSMakeRange(0, backedTextInputViewTextCopy.length)]; - if (eventLag == 0 && ![attributedText isEqualToAttributedString:self.backedTextInputView.attributedText]) { + [attributedTextCopy removeAttribute:RCTTextAttributesTagAttributeName + range:NSMakeRange(0, attributedTextCopy.length)]; + + textNeedsUpdate = ([self textOf:attributedTextCopy equals:backedTextInputViewTextCopy] == NO); + + if (eventLag == 0 && textNeedsUpdate) { UITextRange *selection = self.backedTextInputView.selectedTextRange; NSInteger oldTextLength = self.backedTextInputView.attributedText.string.length; @@ -111,13 +143,13 @@ - (void)setAttributedText:(NSAttributedString *)attributedText if (selection.empty) { // Maintaining a cursor position relative to the end of the old text. NSInteger offsetStart = - [self.backedTextInputView offsetFromPosition:self.backedTextInputView.beginningOfDocument - toPosition:selection.start]; + [self.backedTextInputView offsetFromPosition:self.backedTextInputView.beginningOfDocument + toPosition:selection.start]; NSInteger offsetFromEnd = oldTextLength - offsetStart; NSInteger newOffset = attributedText.string.length - offsetFromEnd; UITextPosition *position = - [self.backedTextInputView positionFromPosition:self.backedTextInputView.beginningOfDocument - offset:newOffset]; + [self.backedTextInputView positionFromPosition:self.backedTextInputView.beginningOfDocument + offset:newOffset]; [self.backedTextInputView setSelectedTextRange:[self.backedTextInputView textRangeFromPosition:position toPosition:position] notifyDelegate:YES]; } @@ -157,6 +189,34 @@ - (void)setSelection:(RCTTextSelection *)selection } } +- (void)setTextContentType:(NSString *)type +{ + #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 + if (@available(iOS 10.0, *)) { + // Setting textContentType to an empty string will disable any + // default behaviour, like the autofill bar for password inputs + self.backedTextInputView.textContentType = [type isEqualToString:@"none"] ? @"" : type; + } + #endif +} + +- (UIKeyboardType)keyboardType +{ + return self.backedTextInputView.keyboardType; +} + +- (void)setKeyboardType:(UIKeyboardType)keyboardType +{ + UIView<RCTBackedTextInputViewProtocol> *textInputView = self.backedTextInputView; + if (textInputView.keyboardType != keyboardType) { + textInputView.keyboardType = keyboardType; + // Without the call to reloadInputViews, the keyboard will not change until the textview field (the first responder) loses and regains focus. + if (textInputView.isFirstResponder) { + [textInputView reloadInputViews]; + } + } +} + #pragma mark - RCTBackedTextInputDelegate - (BOOL)textInputShouldBeginEditing @@ -268,7 +328,12 @@ - (BOOL)textInputShouldChangeTextInRange:(NSRange)range replacementText:(NSStrin NSString *previousText = [_predictedText substringWithRange:range] ?: @""; - if (_predictedText) { + // After clearing the text by replacing it with an empty string, `_predictedText` + // still preserves the deleted text. + // As the first character in the TextInput always comes with the range value (0, 0), + // we should check the range value in order to avoid appending a character to the deleted string + // (which caused the issue #18374) + if (!NSEqualRanges(range, NSMakeRange(0, 0)) && _predictedText) { _predictedText = [_predictedText stringByReplacingCharactersInRange:range withString:text]; } else { _predictedText = text; @@ -419,7 +484,7 @@ - (void)setCustomInputAccessoryViewWithNativeID:(NSString *)nativeID UIView *accessoryView = [strongSelf->_bridge.uiManager viewForNativeID:nativeID withRootTag:rootView.reactTag]; if (accessoryView && [accessoryView isKindOfClass:[RCTInputAccessoryView class]]) { - strongSelf.backedTextInputView.inputAccessoryView = ((RCTInputAccessoryView *)accessoryView).content.inputAccessoryView; + strongSelf.backedTextInputView.inputAccessoryView = ((RCTInputAccessoryView *)accessoryView).inputAccessoryView; [strongSelf reloadInputViewsIfNecessary]; } } diff --git a/RNTester/js/TextInputExample.ios.js b/RNTester/js/TextInputExample.ios.js index 3aa7ec3d98426d..2329a143910697 100644 --- a/RNTester/js/TextInputExample.ios.js +++ b/RNTester/js/TextInputExample.ios.js @@ -4,23 +4,17 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * + * @format * @flow - * @providesModule TextInputExample */ + 'use strict'; -const Button = require('../../Libraries/Components/Button'); -const InputAccessoryView = require('../../Libraries/Components/TextInput/InputAccessoryView'); +const Button = require('Button'); +const InputAccessoryView = require('InputAccessoryView'); var React = require('react'); var ReactNative = require('react-native'); -var { - Text, - TextInput, - View, - StyleSheet, - Slider, - Switch, -} = ReactNative; +var {Text, TextInput, View, StyleSheet, Slider, Switch} = ReactNative; class WithLabel extends React.Component<$FlowFixMeProps> { render() { @@ -43,8 +37,8 @@ class TextEventsExample extends React.Component<{}, $FlowFixMeState> { prev3Text: '<No Event>', }; - updateText = (text) => { - this.setState((state) => { + updateText = text => { + this.setState(state => { return { curText: text, prevText: state.curText, @@ -63,27 +57,31 @@ class TextEventsExample extends React.Component<{}, $FlowFixMeState> { autoCorrect={false} onFocus={() => this.updateText('onFocus')} onBlur={() => this.updateText('onBlur')} - onChange={(event) => this.updateText( - 'onChange text: ' + event.nativeEvent.text - )} - onEndEditing={(event) => this.updateText( - 'onEndEditing text: ' + event.nativeEvent.text - )} - onSubmitEditing={(event) => this.updateText( - 'onSubmitEditing text: ' + event.nativeEvent.text - )} - onSelectionChange={(event) => this.updateText( - 'onSelectionChange range: ' + - event.nativeEvent.selection.start + ',' + - event.nativeEvent.selection.end - )} - onKeyPress={(event) => { + onChange={event => + this.updateText('onChange text: ' + event.nativeEvent.text) + } + onEndEditing={event => + this.updateText('onEndEditing text: ' + event.nativeEvent.text) + } + onSubmitEditing={event => + this.updateText('onSubmitEditing text: ' + event.nativeEvent.text) + } + onSelectionChange={event => + this.updateText( + 'onSelectionChange range: ' + + event.nativeEvent.selection.start + + ',' + + event.nativeEvent.selection.end, + ) + } + onKeyPress={event => { this.updateText('onKeyPress key: ' + event.nativeEvent.key); }} style={styles.default} /> <Text style={styles.eventLabel}> - {this.state.curText}{'\n'} + {this.state.curText} + {'\n'} (prev: {this.state.prevText}){'\n'} (prev2: {this.state.prev2Text}){'\n'} (prev3: {this.state.prev3Text}) @@ -136,7 +134,7 @@ class RewriteExample extends React.Component<$FlowFixMeProps, any> { <TextInput multiline={false} maxLength={limit} - onChangeText={(text) => { + onChangeText={text => { text = text.replace(/ /g, '_'); this.setState({text}); }} @@ -151,7 +149,10 @@ class RewriteExample extends React.Component<$FlowFixMeProps, any> { } } -class RewriteExampleInvalidCharacters extends React.Component<$FlowFixMeProps, any> { +class RewriteExampleInvalidCharacters extends React.Component< + $FlowFixMeProps, + any, + > { constructor(props) { super(props); this.state = {text: ''}; @@ -161,7 +162,7 @@ class RewriteExampleInvalidCharacters extends React.Component<$FlowFixMeProps, a <View style={styles.rewriteContainer}> <TextInput multiline={false} - onChangeText={(text) => { + onChangeText={text => { this.setState({text: text.replace(/\s/g, '')}); }} style={styles.default} @@ -172,19 +173,62 @@ class RewriteExampleInvalidCharacters extends React.Component<$FlowFixMeProps, a } } +class RewriteExampleKana extends React.Component<$FlowFixMeProps, any> { + constructor(props) { + super(props); + this.state = {text: ''}; + } + render() { + return ( + <View style={styles.rewriteContainer}> + <TextInput + multiline={false} + onChangeText={text => { + this.setState({text: text.replace(/ひ/g, '日')}); + }} + style={styles.default} + value={this.state.text} + /> + </View> + ); + } +} + +class SecureEntryExample extends React.Component<$FlowFixMeProps, any> { + constructor(props) { + super(props); + this.state = {text: ''}; + } + render() { + return ( + <View> + <TextInput + secureTextEntry={true} + style={styles.default} + defaultValue="abc" + onChangeText={text => this.setState({text})} + value={this.state.text} + /> + <Text>Current text is: {this.state.text}</Text> + </View> + ); + } +} + class TokenizedTextExample extends React.Component<$FlowFixMeProps, any> { constructor(props) { super(props); this.state = {text: 'Hello #World'}; } render() { - //define delimiter let delimiter = /\s+/; //split string let _text = this.state.text; - let token, index, parts = []; + let token, + index, + parts = []; while (_text) { delimiter.lastIndex = 0; token = delimiter.exec(_text); @@ -203,9 +247,13 @@ class TokenizedTextExample extends React.Component<$FlowFixMeProps, any> { parts.push(_text); //highlight hashtags - parts = parts.map((text) => { + parts = parts.map(text => { if (/^#/.test(text)) { - return <Text key={text} style={styles.hashtag}>{text}</Text>; + return ( + <Text key={text} style={styles.hashtag}> + {text} + </Text> + ); } else { return text; } @@ -216,7 +264,7 @@ class TokenizedTextExample extends React.Component<$FlowFixMeProps, any> { <TextInput multiline={true} style={styles.multiline} - onChangeText={(text) => { + onChangeText={text => { this.setState({text}); }}> <Text>{parts}</Text> @@ -227,7 +275,7 @@ class TokenizedTextExample extends React.Component<$FlowFixMeProps, any> { } class BlurOnSubmitExample extends React.Component<{}> { - focusNextField = (nextField) => { + focusNextField = nextField => { this.refs[nextField].focus(); }; @@ -282,21 +330,24 @@ class BlurOnSubmitExample extends React.Component<{}> { } type SelectionExampleState = { - selection: { - start: number; - end?: number; - }; - value: string; + selection: {| + start: number, + end?: number, + |}, + value: string, }; -class SelectionExample extends React.Component<$FlowFixMeProps, SelectionExampleState> { +class SelectionExample extends React.Component< + $FlowFixMeProps, + SelectionExampleState, + > { _textInput: any; constructor(props) { super(props); this.state = { selection: {start: 0, end: 0}, - value: props.value + value: props.value, }; } @@ -315,7 +366,9 @@ class SelectionExample extends React.Component<$FlowFixMeProps, SelectionExample } selectRandom() { - var positions = [this.getRandomPosition(), this.getRandomPosition()].sort((a, b) => a - b); + var positions = [this.getRandomPosition(), this.getRandomPosition()].sort( + (a, b) => a - b, + ); this.select(...positions); } @@ -334,7 +387,7 @@ class SelectionExample extends React.Component<$FlowFixMeProps, SelectionExample <View> <TextInput multiline={this.props.multiline} - onChangeText={(value) => this.setState({value})} + onChangeText={value => this.setState({value})} onSelectionChange={this.onSelectionChange.bind(this)} ref={textInput => (this._textInput = textInput)} selection={this.state.selection} @@ -342,31 +395,26 @@ class SelectionExample extends React.Component<$FlowFixMeProps, SelectionExample value={this.state.value} /> <View> - <Text> - selection = {JSON.stringify(this.state.selection)} - </Text> + <Text>selection = {JSON.stringify(this.state.selection)}</Text> <Text onPress={this.placeAt.bind(this, 0)}> Place at Start (0, 0) </Text> <Text onPress={this.placeAt.bind(this, length)}> Place at End ({length}, {length}) </Text> - <Text onPress={this.placeAtRandom.bind(this)}> - Place at Random - </Text> - <Text onPress={this.select.bind(this, 0, length)}> - Select All - </Text> - <Text onPress={this.selectRandom.bind(this)}> - Select Random - </Text> + <Text onPress={this.placeAtRandom.bind(this)}>Place at Random</Text> + <Text onPress={this.select.bind(this, 0, length)}>Select All</Text> + <Text onPress={this.selectRandom.bind(this)}>Select Random</Text> </View> </View> ); } } -class AutogrowingTextInputExample extends React.Component<$FlowFixMeProps, $FlowFixMeState> { +class AutogrowingTextInputExample extends React.Component< + $FlowFixMeProps, + $FlowFixMeState, + > { constructor(props) { super(props); @@ -397,20 +445,22 @@ class AutogrowingTextInputExample extends React.Component<$FlowFixMeProps, $Flow minimumValue={0} maximumValue={100} step={10} - onValueChange={(value) => this.setState({width: value})} + onValueChange={value => this.setState({width: value})} /> <Text>Multiline:</Text> <Switch value={this.state.multiline} - onValueChange={(value) => this.setState({multiline: value})} + onValueChange={value => this.setState({multiline: value})} /> <Text>TextInput:</Text> <TextInput value="prop" multiline={this.state.multiline} style={[style, {width: this.state.width + '%'}]} - onChangeText={(value) => this.setState({text: value})} - onContentSizeChange={(event) => this.setState({contentSize: event.nativeEvent.contentSize})} + onChangeText={value => this.setState({text: value})} + onContentSizeChange={event => + this.setState({contentSize: event.nativeEvent.contentSize}) + } {...props} /> <Text>Plain text value representation:</Text> @@ -502,25 +552,31 @@ exports.examples = [ accessibilityLabel="I am the accessibility label for text input" /> ); - } + }, }, { title: "Live Re-Write (<sp> -> '_') + maxLength", render: function() { return <RewriteExample />; - } + }, }, { title: 'Live Re-Write (no spaces allowed)', render: function() { return <RewriteExampleInvalidCharacters />; - } + }, + }, + { + title: 'Live Re-Write (ひ -> 日)', + render: function() { + return <RewriteExampleKana />; + }, }, { title: 'Keyboard Accessory View', render: function() { return <TextInputAccessoryViewExample />; - } + }, }, { title: 'Auto-capitalize', @@ -528,32 +584,20 @@ exports.examples = [ return ( <View> <WithLabel label="none"> - <TextInput - autoCapitalize="none" - style={styles.default} - /> + <TextInput autoCapitalize="none" style={styles.default} /> </WithLabel> <WithLabel label="sentences"> - <TextInput - autoCapitalize="sentences" - style={styles.default} - /> + <TextInput autoCapitalize="sentences" style={styles.default} /> </WithLabel> <WithLabel label="words"> - <TextInput - autoCapitalize="words" - style={styles.default} - /> + <TextInput autoCapitalize="words" style={styles.default} /> </WithLabel> <WithLabel label="characters"> - <TextInput - autoCapitalize="characters" - style={styles.default} - /> + <TextInput autoCapitalize="characters" style={styles.default} /> </WithLabel> </View> ); - } + }, }, { title: 'Auto-correct', @@ -568,7 +612,7 @@ exports.examples = [ </WithLabel> </View> ); - } + }, }, { title: 'Nested content and `value` property', @@ -583,7 +627,10 @@ exports.examples = [ </TextInput> </WithLabel> <WithLabel label="multiline"> - <TextInput style={styles.default} multiline={true} value="(value property)"> + <TextInput + style={styles.default} + multiline={true} + value="(value property)"> (first raw text node) <Text color="red">(internal raw text node)</Text> (last raw text node) @@ -591,7 +638,7 @@ exports.examples = [ </WithLabel> </View> ); - } + }, }, { title: 'Keyboard types', @@ -610,39 +657,29 @@ exports.examples = [ 'web-search', 'numeric', ]; - var examples = keyboardTypes.map((type) => { + var examples = keyboardTypes.map(type => { return ( <WithLabel key={type} label={type}> - <TextInput - keyboardType={type} - style={styles.default} - /> + <TextInput keyboardType={type} style={styles.default} /> </WithLabel> ); }); return <View>{examples}</View>; - } + }, }, { title: 'Keyboard appearance', render: function() { - var keyboardAppearance = [ - 'default', - 'light', - 'dark', - ]; - var examples = keyboardAppearance.map((type) => { + var keyboardAppearance = ['default', 'light', 'dark']; + var examples = keyboardAppearance.map(type => { return ( <WithLabel key={type} label={type}> - <TextInput - keyboardAppearance={type} - style={styles.default} - /> + <TextInput keyboardAppearance={type} style={styles.default} /> </WithLabel> ); }); return <View>{examples}</View>; - } + }, }, { title: 'Return key types', @@ -660,18 +697,15 @@ exports.examples = [ 'done', 'emergency-call', ]; - var examples = returnKeyTypes.map((type) => { + var examples = returnKeyTypes.map(type => { return ( <WithLabel key={type} label={type}> - <TextInput - returnKeyType={type} - style={styles.default} - /> + <TextInput returnKeyType={type} style={styles.default} /> </WithLabel> ); }); return <View>{examples}</View>; - } + }, }, { title: 'Enable return key automatically', @@ -679,27 +713,26 @@ exports.examples = [ return ( <View> <WithLabel label="true"> - <TextInput enablesReturnKeyAutomatically={true} style={styles.default} /> + <TextInput + enablesReturnKeyAutomatically={true} + style={styles.default} + /> </WithLabel> </View> ); - } + }, }, { title: 'Secure text entry', render: function() { - return ( - <View> - <WithLabel label="true"> - <TextInput secureTextEntry={true} style={styles.default} defaultValue="abc" /> - </WithLabel> - </View> - ); - } + return <SecureEntryExample />; + }, }, { title: 'Event handling', - render: function(): React.Element<any> { return <TextEventsExample />; }, + render: function(): React.Element<any> { + return <TextEventsExample />; + }, }, { title: 'Colored input text', @@ -716,7 +749,7 @@ exports.examples = [ /> </View> ); - } + }, }, { title: 'Colored highlight/cursor for text input', @@ -735,24 +768,18 @@ exports.examples = [ /> </View> ); - } + }, }, { title: 'Clear button mode', - render: function () { + render: function() { return ( <View> <WithLabel label="never"> - <TextInput - style={styles.default} - clearButtonMode="never" - /> + <TextInput style={styles.default} clearButtonMode="never" /> </WithLabel> <WithLabel label="while editing"> - <TextInput - style={styles.default} - clearButtonMode="while-editing" - /> + <TextInput style={styles.default} clearButtonMode="while-editing" /> </WithLabel> <WithLabel label="unless editing"> <TextInput @@ -761,14 +788,11 @@ exports.examples = [ /> </WithLabel> <WithLabel label="always"> - <TextInput - style={styles.default} - clearButtonMode="always" - /> + <TextInput style={styles.default} clearButtonMode="always" /> </WithLabel> </View> ); - } + }, }, { title: 'Clear and select', @@ -811,11 +835,13 @@ exports.examples = [ </WithLabel> </View> ); - } + }, }, { title: 'Blur on submit', - render: function(): React.Element<any> { return <BlurOnSubmitExample />; }, + render: function(): React.Element<any> { + return <BlurOnSubmitExample />; + }, }, { title: 'Multiline blur on submit', @@ -832,7 +858,7 @@ exports.examples = [ /> </View> ); - } + }, }, { title: 'Multiline', @@ -873,17 +899,9 @@ exports.examples = [ style={styles.multiline} dataDetectorTypes="phoneNumber" /> - <TextInput - placeholder="multiline with children" - multiline={true} - enablesReturnKeyAutomatically={true} - returnKeyType="go" - style={styles.multiline}> - <View style={styles.multilineChild}/> - </TextInput> </View> ); - } + }, }, { title: 'TextInput Intrinsic Size', @@ -923,7 +941,7 @@ exports.examples = [ borderBottomRightRadius: 20, padding: 10, paddingTop: 20, - maxHeight: 100 + maxHeight: 100, }} testID="multiline_textinput" multiline={true} @@ -950,7 +968,7 @@ exports.examples = [ </View> </View> ); - } + }, }, { title: 'Auto-expanding', @@ -967,7 +985,7 @@ exports.examples = [ /> </View> ); - } + }, }, { title: 'Auto-expanding', @@ -978,11 +996,14 @@ exports.examples = [ enablesReturnKeyAutomatically={true} returnKeyType="done" multiline={true} - style={{maxHeight: 400, minHeight: 20, paddingTop: 0, backgroundColor: '#eeeeee', color: 'blue'}} - > - <Text style={{fontSize: 30, color: 'green'}}> - huge - </Text> + style={{ + maxHeight: 400, + minHeight: 20, + paddingTop: 0, + backgroundColor: '#eeeeee', + color: 'blue', + }}> + <Text style={{fontSize: 30, color: 'green'}}>huge</Text> generic generic generic <Text style={{fontSize: 6, color: 'red'}}> small small small small small small @@ -995,13 +1016,13 @@ exports.examples = [ </AutogrowingTextInputExample> </View> ); - } + }, }, { title: 'Attributed text', render: function() { return <TokenizedTextExample />; - } + }, }, { title: 'Text selection & cursor placement', @@ -1019,7 +1040,7 @@ exports.examples = [ /> </View> ); - } + }, }, { title: 'TextInput maxLength', @@ -1027,10 +1048,7 @@ exports.examples = [ return ( <View> <WithLabel label="maxLength: 5"> - <TextInput - maxLength={5} - style={styles.default} - /> + <TextInput maxLength={5} style={styles.default} /> </WithLabel> <WithLabel label="maxLength: 5 with placeholder"> <TextInput @@ -1055,6 +1073,6 @@ exports.examples = [ </WithLabel> </View> ); - } + }, }, ];