feat(text-input): [iOS] inputAccessoryViewButtonLabel prop (#47441)

Summary:
Fixes https://github.com/facebook/react-native/issues/29244, also mentioned in https://github.com/facebook/react-native/issues/25009

As mentioned in the linked issues, the current return key label in the input accessory view is not localized. In the code, right now the texts are hardcoded (see: [RCTTextInputComponentView.mm](https://github.com/facebook/react-native/blob/main/packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm#L552) and [RCTBaseTextInputView.mm](https://github.com/facebook/react-native/blob/main/packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm#L694)). I could not find the historical reason for this, but doing some investigation there doesn't seem to be a way of getting the translated text into the code by using the existing props. (Ref: https://stackoverflow.com/a/58190342/5415299)

The solution: adding a new property `inputAccessoryViewButtonLabel` which can be used to overwrite these defaults non-translated values. The property is optional to avoid breaking changes.

The implementation works for both Fabric and Paper.

## Changelog:

[IOS] [ADDED] - TextInput `inputAccessoryViewButtonLabel` prop

Pull Request resolved: https://github.com/facebook/react-native/pull/47441

Test Plan:
A new example has been added under the `TextInput` examples in the RNTester. See below:

<details>
<summary>Video demonstrating how the new prop behaves</summary>

https://github.com/user-attachments/assets/b15cb8b8-494a-4f41-b434-e33eeef5d267

</details>

Reviewed By: cipolleschi

Differential Revision: D65533493

Pulled By: javache

fbshipit-source-id: d80bf501ba3e38bf3b09833170780df45a26bb61
This commit is contained in:
Mateo Guzmán 2024-11-13 06:44:23 -08:00 committed by Facebook GitHub Bot
parent fc24171416
commit 32931466ed
14 changed files with 73 additions and 4 deletions

View File

@ -121,6 +121,7 @@ const RCTTextInputViewConfig = {
},
editable: true,
inputAccessoryViewID: true,
inputAccessoryViewButtonLabel: true,
caretHidden: true,
enablesReturnKeyAutomatically: true,
placeholderTextColor: {

View File

@ -266,6 +266,12 @@ type IOSProps = $ReadOnly<{|
*/
inputAccessoryViewID?: ?string,
/**
* An optional label that overrides the default input accessory view button label.
* @platform ios
*/
inputAccessoryViewButtonLabel?: ?string,
/**
* Determines the color of the keyboard.
* @platform ios

View File

@ -310,6 +310,12 @@ type IOSProps = $ReadOnly<{|
*/
inputAccessoryViewID?: ?string,
/**
* An optional label that overrides the default input accessory view button label.
* @platform ios
*/
inputAccessoryViewButtonLabel?: ?string,
/**
* Determines the color of the keyboard.
* @platform ios

View File

@ -37,6 +37,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, assign) BOOL caretHidden;
@property (nonatomic, strong, nullable) NSString *inputAccessoryViewID;
@property (nonatomic, strong, nullable) NSString *inputAccessoryViewButtonLabel;
@end

View File

@ -32,6 +32,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, assign) UITextFieldViewMode clearButtonMode;
@property (nonatomic, getter=isScrollEnabled) BOOL scrollEnabled;
@property (nonatomic, strong, nullable) NSString *inputAccessoryViewID;
@property (nonatomic, strong, nullable) NSString *inputAccessoryViewButtonLabel;
@property (nonatomic, assign, readonly) CGFloat zoomScale;
@property (nonatomic, assign, readonly) CGPoint contentOffset;
@property (nonatomic, assign, readonly) UIEdgeInsets contentInset;

View File

@ -49,6 +49,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, strong, nullable) NSNumber *maxLength;
@property (nonatomic, copy, nullable) NSAttributedString *attributedText;
@property (nonatomic, copy) NSString *inputAccessoryViewID;
@property (nonatomic, strong) NSString *inputAccessoryViewButtonLabel;
@property (nonatomic, assign) UIKeyboardType keyboardType;
@property (nonatomic, assign) BOOL showSoftInputOnFocus;

View File

@ -384,6 +384,16 @@ RCT_NOT_IMPLEMENTED(-(instancetype)initWithFrame : (CGRect)frame)
}
}
- (NSString *)inputAccessoryViewButtonLabel
{
return self.backedTextInputView.inputAccessoryViewButtonLabel;
}
- (void)setInputAccessoryViewButtonLabel:(NSString *)inputAccessoryViewButtonLabel
{
self.backedTextInputView.inputAccessoryViewButtonLabel = inputAccessoryViewButtonLabel;
}
#pragma mark - RCTBackedTextInputDelegate
- (BOOL)textInputShouldBeginEditing
@ -715,6 +725,7 @@ RCT_NOT_IMPLEMENTED(-(instancetype)initWithFrame : (CGRect)frame)
{
UIView<RCTBackedTextInputViewProtocol> *textInputView = self.backedTextInputView;
UIKeyboardType keyboardType = textInputView.keyboardType;
NSString *inputAccessoryViewButtonLabel = textInputView.inputAccessoryViewButtonLabel;
// These keyboard types (all are number pads) don't have a Return Key button by default,
// so we create an `inputAccessoryView` with this button for them.
@ -722,11 +733,12 @@ RCT_NOT_IMPLEMENTED(-(instancetype)initWithFrame : (CGRect)frame)
UIReturnKeyType returnKeyType = textInputView.returnKeyType;
BOOL containsKeyType = [returnKeyTypesSet containsObject:@(returnKeyType)];
BOOL containsInputAccessoryViewButtonLabel = inputAccessoryViewButtonLabel != nil;
BOOL shouldHaveInputAccessoryView =
(keyboardType == UIKeyboardTypeNumberPad || keyboardType == UIKeyboardTypePhonePad ||
keyboardType == UIKeyboardTypeDecimalPad || keyboardType == UIKeyboardTypeASCIICapableNumberPad) &&
containsKeyType;
(containsKeyType || containsInputAccessoryViewButtonLabel);
if (_hasInputAccessoryView == shouldHaveInputAccessoryView) {
return;
@ -735,7 +747,8 @@ RCT_NOT_IMPLEMENTED(-(instancetype)initWithFrame : (CGRect)frame)
_hasInputAccessoryView = shouldHaveInputAccessoryView;
if (shouldHaveInputAccessoryView) {
NSString *buttonLabel = [self returnKeyTypeToString:returnKeyType];
NSString *buttonLabel = inputAccessoryViewButtonLabel != nil ? inputAccessoryViewButtonLabel
: [self returnKeyTypeToString:returnKeyType];
UIToolbar *toolbarView = [UIToolbar new];
[toolbarView sizeToFit];

View File

@ -57,6 +57,7 @@ RCT_EXPORT_VIEW_PROPERTY(maxLength, NSNumber)
RCT_EXPORT_VIEW_PROPERTY(selectTextOnFocus, BOOL)
RCT_EXPORT_VIEW_PROPERTY(selection, RCTTextSelection)
RCT_EXPORT_VIEW_PROPERTY(inputAccessoryViewID, NSString)
RCT_EXPORT_VIEW_PROPERTY(inputAccessoryViewButtonLabel, NSString)
RCT_EXPORT_VIEW_PROPERTY(textContentType, NSString)
RCT_EXPORT_VIEW_PROPERTY(passwordRules, NSString)

View File

@ -30,6 +30,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, assign, getter=isEditable) BOOL editable;
@property (nonatomic, getter=isScrollEnabled) BOOL scrollEnabled;
@property (nonatomic, strong, nullable) NSString *inputAccessoryViewID;
@property (nonatomic, strong, nullable) NSString *inputAccessoryViewButtonLabel;
@property (nonatomic, assign, readonly) CGFloat zoomScale;
@property (nonatomic, assign, readonly) CGPoint contentOffset;
@property (nonatomic, assign, readonly) UIEdgeInsets contentInset;

View File

@ -2962,6 +2962,7 @@ type IOSProps = $ReadOnly<{|
| $ReadOnlyArray<DataDetectorTypesType>,
enablesReturnKeyAutomatically?: ?boolean,
inputAccessoryViewID?: ?string,
inputAccessoryViewButtonLabel?: ?string,
keyboardAppearance?: ?(\\"default\\" | \\"light\\" | \\"dark\\"),
passwordRules?: ?PasswordRules,
rejectResponderTermination?: ?boolean,
@ -3314,6 +3315,7 @@ type IOSProps = $ReadOnly<{|
| $ReadOnlyArray<DataDetectorTypesType>,
enablesReturnKeyAutomatically?: ?boolean,
inputAccessoryViewID?: ?string,
inputAccessoryViewButtonLabel?: ?string,
keyboardAppearance?: ?(\\"default\\" | \\"light\\" | \\"dark\\"),
passwordRules?: ?PasswordRules,
rejectResponderTermination?: ?boolean,

View File

@ -273,6 +273,11 @@ static NSSet<NSNumber *> *returnKeyTypesSet;
if (newTextInputProps.inputAccessoryViewID != oldTextInputProps.inputAccessoryViewID) {
_backedTextInputView.inputAccessoryViewID = RCTNSStringFromString(newTextInputProps.inputAccessoryViewID);
}
if (newTextInputProps.inputAccessoryViewButtonLabel != oldTextInputProps.inputAccessoryViewButtonLabel) {
_backedTextInputView.inputAccessoryViewButtonLabel =
RCTNSStringFromString(newTextInputProps.inputAccessoryViewButtonLabel);
}
[super updateProps:props oldProps:oldProps];
[self setDefaultInputAccessoryView];
@ -581,22 +586,25 @@ static NSSet<NSNumber *> *returnKeyTypesSet;
UIKeyboardType keyboardType = _backedTextInputView.keyboardType;
UIReturnKeyType returnKeyType = _backedTextInputView.returnKeyType;
NSString *inputAccessoryViewButtonLabel = _backedTextInputView.inputAccessoryViewButtonLabel;
BOOL containsKeyType = [returnKeyTypesSet containsObject:@(returnKeyType)];
BOOL containsInputAccessoryViewButtonLabel = inputAccessoryViewButtonLabel != nil;
// These keyboard types (all are number pads) don't have a "returnKey" button by default,
// so we create an `inputAccessoryView` with this button for them.
BOOL shouldHaveInputAccessoryView =
(keyboardType == UIKeyboardTypeNumberPad || keyboardType == UIKeyboardTypePhonePad ||
keyboardType == UIKeyboardTypeDecimalPad || keyboardType == UIKeyboardTypeASCIICapableNumberPad) &&
containsKeyType;
(containsKeyType || containsInputAccessoryViewButtonLabel);
if ((_backedTextInputView.inputAccessoryView != nil) == shouldHaveInputAccessoryView) {
return;
}
if (shouldHaveInputAccessoryView) {
NSString *buttonLabel = [self returnKeyTypeToString:returnKeyType];
NSString *buttonLabel = inputAccessoryViewButtonLabel != nil ? inputAccessoryViewButtonLabel
: [self returnKeyTypeToString:returnKeyType];
UIToolbar *toolbarView = [UIToolbar new];
[toolbarView sizeToFit];

View File

@ -33,6 +33,12 @@ TextInputProps::TextInputProps(
"inputAccessoryViewID",
sourceProps.inputAccessoryViewID,
{})),
inputAccessoryViewButtonLabel(convertRawProp(
context,
rawProps,
"inputAccessoryViewButtonLabel",
sourceProps.inputAccessoryViewButtonLabel,
{})),
onKeyPressSync(convertRawProp(
context,
rawProps,

View File

@ -37,6 +37,7 @@ class TextInputProps final : public BaseTextInputProps {
std::optional<Selection> selection{};
const std::string inputAccessoryViewID{};
const std::string inputAccessoryViewButtonLabel{};
bool onKeyPressSync{false};
bool onChangeSync{false};

View File

@ -349,6 +349,27 @@ const textInputExamples: Array<RNTesterModuleExample> = [
return <View>{examples}</View>;
},
},
{
title: 'Custom Input Accessory View Button Label',
render: function (): React.Node {
return (
<View>
<WithLabel label="Localized Label">
<ExampleTextInput
keyboardType="number-pad"
inputAccessoryViewButtonLabel="Presiona aquí para terminar"
/>
</WithLabel>
<WithLabel label="Custom Label">
<ExampleTextInput
keyboardType="ascii-capable-number-pad"
inputAccessoryViewButtonLabel="Press here to finish"
/>
</WithLabel>
</View>
);
},
},
{
title: 'Nested content and `value` property',
render: function (): React.Node {