Skip to content

Commit 0154372

Browse files
rezkiy37facebook-github-bot
authored andcommitted
feat: Manage keyboard shortcuts visibility of TextInput (#47671)
Summary: **iOS** does offer a native property for **UITextField** called `inputAssistantItem`. According to the [documentation](https://developer.apple.com/documentation/uikit/uitextinputassistantitem), we can hide the **"shortcuts"** by setting the `leadingBarButtonGroups` and `trailingBarButtonGroups` properties to empty arrays. I propose adding a new property for **TextInput** in **React Native**, which would set these native properties to empty arrays. This new property could be called `disableInputAssistant` or `disableKeyboardShortcuts` and would be a `boolean`. Developers can manage this behavior (the redo & undo buttons and suggestions pop-up hiding) after applying these native props. react-native-community/discussions-and-proposals#830 ## Changelog: <!-- Help reviewers and the release process by writing your own changelog entry. Pick one each for the category and type tags: [IOS] [ADDED] - [TextInput] Integrate a new property - `disableKeyboardShortcuts`. It can disable the keyboard shortcuts on iPads. For more details, see: https://reactnative.dev/contributing/changelogs-in-pull-requests --> [IOS] [ADDED] - [TextInput] Integrate a new property - `disableKeyboardShortcuts`. It can disable the keyboard shortcuts on iPads. Pull Request resolved: #47671 Test Plan: Manual 1. Open TextInput examples. 2. Scroll down and reach the "Keyboard shortcuts" section. 3. Test each case. Note: **TextInput** behaves the same as now when the new prop is not passed or is `false`. https://github.com/user-attachments/assets/5e814516-9e6c-4495-9d46-8175425c4456 Reviewed By: javache Differential Revision: D67451609 Pulled By: cipolleschi fbshipit-source-id: 59ba3a5cc1644ed176420f82dc98232d88341c6e
1 parent a3dfc49 commit 0154372

File tree

17 files changed

+186
-0
lines changed

17 files changed

+186
-0
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ const RCTTextInputViewConfig = {
162162
onChangeSync: true,
163163
onKeyPressSync: true,
164164
}),
165+
disableKeyboardShortcuts: true,
165166
},
166167
};
167168

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,11 @@ export interface DocumentSelectionState extends EventEmitter {
136136
* @see https://reactnative.dev/docs/textinput#props
137137
*/
138138
export interface TextInputIOSProps {
139+
/**
140+
* If true, the keyboard shortcuts (undo/redo and copy buttons) are disabled. The default value is false.
141+
*/
142+
disableKeyboardShortcuts?: boolean | undefined;
143+
139144
/**
140145
* enum('never', 'while-editing', 'unless-editing', 'always')
141146
* When the clear button should appear on the right side of the text view

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,12 @@ export type enterKeyHintType =
216216
type PasswordRules = string;
217217

218218
type IOSProps = $ReadOnly<{|
219+
/**
220+
* If true, the keyboard shortcuts (undo/redo and copy buttons) are disabled. The default value is false.
221+
* @platform ios
222+
*/
223+
disableKeyboardShortcuts?: ?boolean,
224+
219225
/**
220226
* When the clear button should appear on the right side of the text view.
221227
* This property is supported only for single-line TextInput component.

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,12 @@ export type enterKeyHintType =
260260
type PasswordRules = string;
261261

262262
type IOSProps = $ReadOnly<{|
263+
/**
264+
* If true, the keyboard shortcuts (undo/redo and copy buttons) are disabled. The default value is false.
265+
* @platform ios
266+
*/
267+
disableKeyboardShortcuts?: ?boolean,
268+
263269
/**
264270
* When the clear button should appear on the right side of the text view.
265271
* This property is supported only for single-line TextInput component.

packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ NS_ASSUME_NONNULL_BEGIN
3939
@property (nonatomic, strong, nullable) NSString *inputAccessoryViewID;
4040
@property (nonatomic, strong, nullable) NSString *inputAccessoryViewButtonLabel;
4141

42+
@property (nonatomic, assign) BOOL disableKeyboardShortcuts;
43+
4244
@end
4345

4446
NS_ASSUME_NONNULL_END

packages/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ @implementation RCTUITextView {
1818
UITextView *_detachedTextView;
1919
RCTBackedTextViewDelegateAdapter *_textInputDelegateAdapter;
2020
NSDictionary<NSAttributedStringKey, id> *_defaultTextAttributes;
21+
NSArray<UIBarButtonItemGroup *> *_initialValueLeadingBarButtonGroups;
22+
NSArray<UIBarButtonItemGroup *> *_initialValueTrailingBarButtonGroups;
2123
}
2224

2325
static UIFont *defaultPlaceholderFont(void)
@@ -52,6 +54,8 @@ - (instancetype)initWithFrame:(CGRect)frame
5254
self.textContainer.lineFragmentPadding = 0;
5355
self.scrollsToTop = NO;
5456
self.scrollEnabled = YES;
57+
_initialValueLeadingBarButtonGroups = nil;
58+
_initialValueTrailingBarButtonGroups = nil;
5559
}
5660

5761
return self;
@@ -132,6 +136,26 @@ - (void)textDidChange
132136
[self _invalidatePlaceholderVisibility];
133137
}
134138

139+
- (void)setDisableKeyboardShortcuts:(BOOL)disableKeyboardShortcuts
140+
{
141+
// Initialize the initial values only once
142+
if (_initialValueLeadingBarButtonGroups == nil) {
143+
// Capture initial values of leading and trailing button groups
144+
_initialValueLeadingBarButtonGroups = self.inputAssistantItem.leadingBarButtonGroups;
145+
_initialValueTrailingBarButtonGroups = self.inputAssistantItem.trailingBarButtonGroups;
146+
}
147+
148+
if (disableKeyboardShortcuts) {
149+
self.inputAssistantItem.leadingBarButtonGroups = @[];
150+
self.inputAssistantItem.trailingBarButtonGroups = @[];
151+
} else {
152+
// Restore the initial values
153+
self.inputAssistantItem.leadingBarButtonGroups = _initialValueLeadingBarButtonGroups;
154+
self.inputAssistantItem.trailingBarButtonGroups = _initialValueTrailingBarButtonGroups;
155+
}
156+
_disableKeyboardShortcuts = disableKeyboardShortcuts;
157+
}
158+
135159
#pragma mark - Overrides
136160

137161
- (void)setFont:(UIFont *)font

packages/react-native/Libraries/Text/TextInput/RCTBackedTextInputViewProtocol.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ NS_ASSUME_NONNULL_BEGIN
5252
// Use `attributedText.string` instead.
5353
@property (nonatomic, copy, nullable) NSString *text NS_UNAVAILABLE;
5454

55+
@property (nonatomic, assign) BOOL disableKeyboardShortcuts;
56+
5557
@end
5658

5759
NS_ASSUME_NONNULL_END

packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ @implementation RCTBaseTextInputView {
3030
BOOL _hasInputAccessoryView;
3131
NSString *_Nullable _predictedText;
3232
BOOL _didMoveToWindow;
33+
NSArray<UIBarButtonItemGroup *> *_initialValueLeadingBarButtonGroups;
34+
NSArray<UIBarButtonItemGroup *> *_initialValueTrailingBarButtonGroups;
3335
}
3436

3537
- (void)reactUpdateResponderOffsetForScrollView:(RCTScrollView *)scrollView
@@ -65,6 +67,8 @@ - (instancetype)initWithBridge:(RCTBridge *)bridge
6567
_bridge = bridge;
6668
_eventDispatcher = bridge.eventDispatcher;
6769
[self initializeReturnKeyType];
70+
_initialValueLeadingBarButtonGroups = nil;
71+
_initialValueTrailingBarButtonGroups = nil;
6872
}
6973

7074
return self;
@@ -394,6 +398,25 @@ - (void)setInputAccessoryViewButtonLabel:(NSString *)inputAccessoryViewButtonLab
394398
self.backedTextInputView.inputAccessoryViewButtonLabel = inputAccessoryViewButtonLabel;
395399
}
396400

401+
- (void)setDisableKeyboardShortcuts:(BOOL)disableKeyboardShortcuts
402+
{
403+
// Initialize the initial values only once
404+
if (_initialValueLeadingBarButtonGroups == nil) {
405+
// Capture initial values of leading and trailing button groups
406+
_initialValueLeadingBarButtonGroups = self.backedTextInputView.inputAssistantItem.leadingBarButtonGroups;
407+
_initialValueTrailingBarButtonGroups = self.backedTextInputView.inputAssistantItem.trailingBarButtonGroups;
408+
}
409+
410+
if (disableKeyboardShortcuts) {
411+
self.backedTextInputView.inputAssistantItem.leadingBarButtonGroups = @[];
412+
self.backedTextInputView.inputAssistantItem.trailingBarButtonGroups = @[];
413+
} else {
414+
// Restore the initial values
415+
self.backedTextInputView.inputAssistantItem.leadingBarButtonGroups = _initialValueLeadingBarButtonGroups;
416+
self.backedTextInputView.inputAssistantItem.trailingBarButtonGroups = _initialValueTrailingBarButtonGroups;
417+
}
418+
}
419+
397420
#pragma mark - RCTBackedTextInputDelegate
398421

399422
- (BOOL)textInputShouldBeginEditing

packages/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ @implementation RCTBaseTextInputViewManager {
6969

7070
RCT_EXPORT_VIEW_PROPERTY(mostRecentEventCount, NSInteger)
7171

72+
RCT_EXPORT_VIEW_PROPERTY(disableKeyboardShortcuts, BOOL)
73+
7274
RCT_EXPORT_SHADOW_PROPERTY(text, NSString)
7375
RCT_EXPORT_SHADOW_PROPERTY(placeholder, NSString)
7476
RCT_EXPORT_SHADOW_PROPERTY(onContentSizeChange, RCTDirectEventBlock)

packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ NS_ASSUME_NONNULL_BEGIN
3434
@property (nonatomic, assign, readonly) CGFloat zoomScale;
3535
@property (nonatomic, assign, readonly) CGPoint contentOffset;
3636
@property (nonatomic, assign, readonly) UIEdgeInsets contentInset;
37+
@property (nonatomic, assign) BOOL disableKeyboardShortcuts;
3738

3839
@end
3940

packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
@implementation RCTUITextField {
1616
RCTBackedTextFieldDelegateAdapter *_textInputDelegateAdapter;
1717
NSDictionary<NSAttributedStringKey, id> *_defaultTextAttributes;
18+
NSArray<UIBarButtonItemGroup *> *_initialValueLeadingBarButtonGroups;
19+
NSArray<UIBarButtonItemGroup *> *_initialValueTrailingBarButtonGroups;
1820
}
1921

2022
- (instancetype)initWithFrame:(CGRect)frame
@@ -27,6 +29,8 @@ - (instancetype)initWithFrame:(CGRect)frame
2729

2830
_textInputDelegateAdapter = [[RCTBackedTextFieldDelegateAdapter alloc] initWithTextField:self];
2931
_scrollEnabled = YES;
32+
_initialValueLeadingBarButtonGroups = nil;
33+
_initialValueTrailingBarButtonGroups = nil;
3034
}
3135

3236
return self;
@@ -115,6 +119,26 @@ - (void)setSecureTextEntry:(BOOL)secureTextEntry
115119
self.attributedText = originalText;
116120
}
117121

122+
- (void)setDisableKeyboardShortcuts:(BOOL)disableKeyboardShortcuts
123+
{
124+
// Initialize the initial values only once
125+
if (_initialValueLeadingBarButtonGroups == nil) {
126+
// Capture initial values of leading and trailing button groups
127+
_initialValueLeadingBarButtonGroups = self.inputAssistantItem.leadingBarButtonGroups;
128+
_initialValueTrailingBarButtonGroups = self.inputAssistantItem.trailingBarButtonGroups;
129+
}
130+
131+
if (disableKeyboardShortcuts) {
132+
self.inputAssistantItem.leadingBarButtonGroups = @[];
133+
self.inputAssistantItem.trailingBarButtonGroups = @[];
134+
} else {
135+
// Restore the initial values
136+
self.inputAssistantItem.leadingBarButtonGroups = _initialValueLeadingBarButtonGroups;
137+
self.inputAssistantItem.trailingBarButtonGroups = _initialValueTrailingBarButtonGroups;
138+
}
139+
_disableKeyboardShortcuts = disableKeyboardShortcuts;
140+
}
141+
118142
#pragma mark - Placeholder
119143

120144
- (NSDictionary<NSAttributedStringKey, id> *)_placeholderTextAttributes

packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3026,6 +3026,7 @@ export type enterKeyHintType =
30263026
| \\"send\\";
30273027
type PasswordRules = string;
30283028
type IOSProps = $ReadOnly<{|
3029+
disableKeyboardShortcuts?: ?boolean,
30293030
clearButtonMode?: ?(\\"never\\" | \\"while-editing\\" | \\"unless-editing\\" | \\"always\\"),
30303031
clearTextOnFocus?: ?boolean,
30313032
dataDetectorTypes?:
@@ -3379,6 +3380,7 @@ export type enterKeyHintType =
33793380
| \\"enter\\";
33803381
type PasswordRules = string;
33813382
type IOSProps = $ReadOnly<{|
3383+
disableKeyboardShortcuts?: ?boolean,
33823384
clearButtonMode?: ?(\\"never\\" | \\"while-editing\\" | \\"unless-editing\\" | \\"always\\"),
33833385
clearTextOnFocus?: ?boolean,
33843386
dataDetectorTypes?:

packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,11 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
285285
_backedTextInputView.inputAccessoryViewButtonLabel =
286286
RCTNSStringFromString(newTextInputProps.inputAccessoryViewButtonLabel);
287287
}
288+
289+
if (newTextInputProps.disableKeyboardShortcuts != oldTextInputProps.disableKeyboardShortcuts) {
290+
_backedTextInputView.disableKeyboardShortcuts = newTextInputProps.disableKeyboardShortcuts;
291+
}
292+
288293
[super updateProps:props oldProps:oldProps];
289294

290295
[self setDefaultInputAccessoryView];

packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputUtils.mm

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ void RCTCopyBackedTextInput(
4545
toTextInput.textContentType = fromTextInput.textContentType;
4646
toTextInput.smartInsertDeleteType = fromTextInput.smartInsertDeleteType;
4747
toTextInput.passwordRules = fromTextInput.passwordRules;
48+
toTextInput.disableKeyboardShortcuts = fromTextInput.disableKeyboardShortcuts;
4849

4950
[toTextInput setSelectedTextRange:fromTextInput.selectedTextRange notifyDelegate:NO];
5051
}

packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputProps.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,12 @@ BaseTextInputProps::BaseTextInputProps(
126126
rawProps,
127127
"multiline",
128128
sourceProps.multiline,
129+
{false})),
130+
disableKeyboardShortcuts(convertRawProp(
131+
context,
132+
rawProps,
133+
"disableKeyboardShortcuts",
134+
sourceProps.disableKeyboardShortcuts,
129135
{false})) {}
130136

131137
void BaseTextInputProps::setProp(
@@ -208,6 +214,7 @@ void BaseTextInputProps::setProp(
208214
RAW_SET_PROP_SWITCH_CASE_BASIC(readOnly);
209215
RAW_SET_PROP_SWITCH_CASE_BASIC(submitBehavior);
210216
RAW_SET_PROP_SWITCH_CASE_BASIC(multiline);
217+
RAW_SET_PROP_SWITCH_CASE_BASIC(disableKeyboardShortcuts);
211218
}
212219
}
213220

packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputProps.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ class BaseTextInputProps : public ViewProps, public BaseTextProps {
8080
SubmitBehavior submitBehavior{SubmitBehavior::Default};
8181

8282
bool multiline{false};
83+
84+
bool disableKeyboardShortcuts{false};
8385
};
8486

8587
} // namespace facebook::react

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

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,73 @@ const TextInputWithFocusButton = () => {
296296
);
297297
};
298298

299+
function KeyboardShortcutsExample() {
300+
return (
301+
<View>
302+
<Text
303+
style={{
304+
marginBottom: 4,
305+
}}>
306+
Single line:
307+
</Text>
308+
309+
<ExampleTextInput
310+
style={{
311+
marginBottom: 4,
312+
}}
313+
placeholder="Default"
314+
/>
315+
316+
<ExampleTextInput
317+
style={{
318+
marginBottom: 4,
319+
}}
320+
placeholder="Disable keyboard shortcuts"
321+
disableKeyboardShortcuts
322+
/>
323+
324+
<ExampleTextInput
325+
style={{
326+
marginBottom: 4,
327+
}}
328+
placeholder="Hidden keyboard with suggestions"
329+
showSoftInputOnFocus={false}
330+
/>
331+
332+
<ExampleTextInput
333+
style={{
334+
marginBottom: 4,
335+
}}
336+
placeholder="Hidden keyboard without suggestions"
337+
disableKeyboardShortcuts
338+
autoCorrect={false}
339+
spellCheck={false}
340+
showSoftInputOnFocus={false}
341+
/>
342+
343+
<Text
344+
style={{
345+
marginBottom: 4,
346+
}}>
347+
Multiline:
348+
</Text>
349+
350+
<ExampleTextInput
351+
style={styles.multiline}
352+
multiline
353+
placeholder="default"
354+
/>
355+
356+
<ExampleTextInput
357+
style={styles.multiline}
358+
multiline
359+
placeholder="Disable keyboard shortcuts"
360+
disableKeyboardShortcuts
361+
/>
362+
</View>
363+
);
364+
}
365+
299366
const styles = StyleSheet.create({
300367
multiline: {
301368
height: 50,
@@ -938,6 +1005,12 @@ const textInputExamples: Array<RNTesterModuleExample> = [
9381005
return <TextInputWithFocusButton />;
9391006
},
9401007
},
1008+
{
1009+
title: 'Keyboard shortcuts',
1010+
render: function (): React.Node {
1011+
return <KeyboardShortcutsExample />;
1012+
},
1013+
},
9411014
{
9421015
title: 'Line Break Mode',
9431016
render: function (): React.Node {

0 commit comments

Comments
 (0)