Skip to content

Commit 9494e08

Browse files
authored
Merge pull request Expensify#61492 from margelo/@chrispader/fix-keyboard-opening-before-emoji-reaction-picker
2 parents 2eaaae2 + 588cb7c commit 9494e08

20 files changed

+490
-116
lines changed

patches/react-native/details.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# `react-native` patches
2+
3+
### [react-native+0.79.2+025+textinput-prevent-focus-on-first-responder.patch](react-native+0.79.2+025+textinput-prevent-focus-on-first-responder.patch)
4+
5+
- Reason: On iOS, a text input automatically becomes the "first responder" in UIKit's "UIResponder" chain. Once a text input becomes the first responder, it will be automatically focused. (This also causes the keyboard ot open)
6+
- This is not handled by React or React Native, but is rather an native iOS/UIKit behaviour. This patch adds additional an additional `TextInput` prop (`preventFocusOnFirstResponder`) and a ref method (`preventFocusOnFirstResponderOnce`) to bypass the focus on first responder.
7+
- In E/App this causes issues with e.g. the keyboard briefly opening after a modal has been dismissed before another modal is opened (`ReportActionContextMenu` -> `EmojiPicker`)
8+
- Upstream PR/issue: None, because this is not a real bug fix but a hotfix specific to Expensify
9+
- E/App issue: [#54813](https://github.com/Expensify/App/issues/54813)
10+
- PR Introducing Patch: [#61492](https://github.com/Expensify/App/pull/61492)
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
diff --git a/node_modules/react-native/Libraries/Components/TextInput/RCTMultilineTextInputNativeComponent.js b/node_modules/react-native/Libraries/Components/TextInput/RCTMultilineTextInputNativeComponent.js
2+
index 8c40af6..2a32cd4 100644
3+
--- a/node_modules/react-native/Libraries/Components/TextInput/RCTMultilineTextInputNativeComponent.js
4+
+++ b/node_modules/react-native/Libraries/Components/TextInput/RCTMultilineTextInputNativeComponent.js
5+
@@ -21,7 +21,7 @@ type NativeType = HostComponent<{...}>;
6+
type NativeCommands = TextInputNativeCommands<NativeType>;
7+
8+
export const Commands: NativeCommands = codegenNativeCommands<NativeCommands>({
9+
- supportedCommands: ['focus', 'blur', 'setTextAndSelection'],
10+
+ supportedCommands: ['focus', 'blur', 'setTextAndSelection', 'preventFocusOnFirstResponderOnce'],
11+
});
12+
13+
export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = {
14+
diff --git a/node_modules/react-native/Libraries/Components/TextInput/RCTSingelineTextInputNativeComponent.js b/node_modules/react-native/Libraries/Components/TextInput/RCTSingelineTextInputNativeComponent.js
15+
index a52be63..820153a 100644
16+
--- a/node_modules/react-native/Libraries/Components/TextInput/RCTSingelineTextInputNativeComponent.js
17+
+++ b/node_modules/react-native/Libraries/Components/TextInput/RCTSingelineTextInputNativeComponent.js
18+
@@ -21,7 +21,7 @@ type NativeType = HostComponent<{...}>;
19+
type NativeCommands = TextInputNativeCommands<NativeType>;
20+
21+
export const Commands: NativeCommands = codegenNativeCommands<NativeCommands>({
22+
- supportedCommands: ['focus', 'blur', 'setTextAndSelection'],
23+
+ supportedCommands: ['focus', 'blur', 'setTextAndSelection', 'preventFocusOnFirstResponderOnce'],
24+
});
25+
26+
export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = {
27+
diff --git a/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js b/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js
28+
index 94b7d31..549ffab 100644
29+
--- a/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js
30+
+++ b/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js
31+
@@ -163,6 +163,7 @@ const RCTTextInputViewConfig = {
32+
lineBreakStrategyIOS: true,
33+
lineBreakModeIOS: true,
34+
smartInsertDelete: true,
35+
+ preventFocusOnFirstResponder: true,
36+
...ConditionallyIgnoredEventHandlers({
37+
onClear: true,
38+
onChange: true,
39+
diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts b/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts
40+
index 2112772..4efd46e 100644
41+
--- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts
42+
+++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts
43+
@@ -770,6 +770,11 @@ export interface TextInputProps
44+
*/
45+
multiline?: boolean | undefined;
46+
47+
+ /**
48+
+ * If true, the text input will not restore focus when the input becomes first responder.
49+
+ */
50+
+ preventFocusOnFirstResponder?: boolean | undefined;
51+
+
52+
/**
53+
* Callback that is called when the text input is blurred
54+
*/
55+
@@ -1033,4 +1038,9 @@ export class TextInput extends TextInputBase {
56+
* Sets the start and end positions of text selection.
57+
*/
58+
setSelection: (start: number, end: number) => void;
59+
+
60+
+ /**
61+
+ * Prevents the text input once from restoring focus when the input becomes first responder.
62+
+ */
63+
+ preventFocusOnFirstResponderOnce: () => void;
64+
}
65+
diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js b/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js
66+
index 50f7794..d5585f0 100644
67+
--- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js
68+
+++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js
69+
@@ -1012,6 +1012,7 @@ export interface TextInputInstance extends HostInstance {
70+
+isFocused: () => boolean;
71+
+getNativeRef: () => ?HostInstance;
72+
+setSelection: (start: number, end: number) => void;
73+
+ +preventFocusOnFirstResponderOnce: () => void;
74+
}
75+
76+
/**
77+
diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.js b/node_modules/react-native/Libraries/Components/TextInput/TextInput.js
78+
index 1fb07fb..1fed107 100644
79+
--- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.js
80+
+++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.js
81+
@@ -1430,6 +1430,11 @@ function InternalTextInput(props: TextInputProps): React.Node {
82+
);
83+
}
84+
},
85+
+ preventFocusOnFirstResponderOnce: () => {
86+
+ if (inputRef.current != null && Platform.OS !== 'android') {
87+
+ viewCommands.preventFocusOnFirstResponderOnce(inputRef.current);
88+
+ }
89+
+ },
90+
});
91+
}
92+
},
93+
diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInputNativeCommands.js b/node_modules/react-native/Libraries/Components/TextInput/TextInputNativeCommands.js
94+
index 9da8899..bf87ffd 100644
95+
--- a/node_modules/react-native/Libraries/Components/TextInput/TextInputNativeCommands.js
96+
+++ b/node_modules/react-native/Libraries/Components/TextInput/TextInputNativeCommands.js
97+
@@ -22,8 +22,9 @@ export interface TextInputNativeCommands<T> {
98+
start: Int32,
99+
end: Int32,
100+
) => void;
101+
+ +preventFocusOnFirstResponderOnce: (viewRef: React.ElementRef<T>) => void;
102+
}
103+
104+
-const supportedCommands = ['focus', 'blur', 'setTextAndSelection'] as string[];
105+
+const supportedCommands = ['focus', 'blur', 'setTextAndSelection', 'preventFocusOnFirstResponderOnce'] as string[];
106+
107+
export default supportedCommands;
108+
diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm
109+
index fef9c63..0a887d5 100644
110+
--- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm
111+
+++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm
112+
@@ -70,6 +70,16 @@ static NSSet<NSNumber *> *returnKeyTypesSet;
113+
NSDictionary<NSAttributedStringKey, id> *_originalTypingAttributes;
114+
115+
BOOL _hasInputAccessoryView;
116+
+
117+
+ /**
118+
+ * Prevents automatic focus of the text input once it becomes the first responder (e.g. on modal dismissal).
119+
+ */
120+
+ BOOL _preventFocusOnFirstResponder;
121+
+
122+
+ /**
123+
+ * Same as above but is only used to prevent focus once
124+
+ */
125+
+ BOOL _preventFocusOnFirstResponderOnce;
126+
}
127+
128+
#pragma mark - UIView overrides
129+
@@ -295,6 +305,10 @@ static NSSet<NSNumber *> *returnKeyTypesSet;
130+
_backedTextInputView.disableKeyboardShortcuts = newTextInputProps.disableKeyboardShortcuts;
131+
}
132+
133+
+ if (newTextInputProps.preventFocusOnFirstResponder != oldTextInputProps.preventFocusOnFirstResponder) {
134+
+ _preventFocusOnFirstResponder = newTextInputProps.preventFocusOnFirstResponder;
135+
+ }
136+
+
137+
[super updateProps:props oldProps:oldProps];
138+
139+
[self setDefaultInputAccessoryView];
140+
@@ -357,6 +371,15 @@ static NSSet<NSNumber *> *returnKeyTypesSet;
141+
142+
- (BOOL)textInputShouldBeginEditing
143+
{
144+
+ if (_preventFocusOnFirstResponderOnce) {
145+
+ _preventFocusOnFirstResponderOnce = NO;
146+
+ return NO;
147+
+ }
148+
+
149+
+ if (_preventFocusOnFirstResponder) {
150+
+ return NO;
151+
+ }
152+
+
153+
return YES;
154+
}
155+
156+
@@ -576,6 +599,11 @@ static NSSet<NSNumber *> *returnKeyTypesSet;
157+
_comingFromJS = NO;
158+
}
159+
160+
+- (void)preventFocusOnFirstResponderOnce
161+
+{
162+
+ _preventFocusOnFirstResponderOnce = YES;
163+
+}
164+
+
165+
#pragma mark - Default input accessory view
166+
167+
- (NSString *)returnKeyTypeToString:(UIReturnKeyType)returnKeyType
168+
diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputNativeCommands.h b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputNativeCommands.h
169+
index f674d98..6ad10ec 100644
170+
--- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputNativeCommands.h
171+
+++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputNativeCommands.h
172+
@@ -19,6 +19,7 @@ NS_ASSUME_NONNULL_BEGIN
173+
value:(NSString *__nullable)value
174+
start:(NSInteger)start
175+
end:(NSInteger)end;
176+
+- (void)preventFocusOnFirstResponderOnce;
177+
@end
178+
179+
RCT_EXTERN inline void
180+
@@ -109,6 +110,19 @@ RCTTextInputHandleCommand(id<RCTTextInputViewProtocol> componentView, const NSSt
181+
return;
182+
}
183+
184+
+ if ([commandName isEqualToString:@"preventFocusOnFirstResponderOnce"]) {
185+
+#if RCT_DEBUG
186+
+ if ([args count] != 0) {
187+
+ RCTLogError(
188+
+ @"%@ command %@ received %d arguments, expected %d.", @"TextInput", commandName, (int)[args count], 0);
189+
+ return;
190+
+ }
191+
+#endif
192+
+
193+
+ [componentView preventFocusOnFirstResponderOnce];
194+
+ return;
195+
+ }
196+
+
197+
#if RCT_DEBUG
198+
RCTLogError(@"%@ received command %@, which is not a supported command.", @"TextInput", commandName);
199+
#endif
200+
diff --git a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputProps.cpp b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputProps.cpp
201+
index 47787a5..f671682 100644
202+
--- a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputProps.cpp
203+
+++ b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputProps.cpp
204+
@@ -127,6 +127,12 @@ BaseTextInputProps::BaseTextInputProps(
205+
"multiline",
206+
sourceProps.multiline,
207+
{false})),
208+
+ preventFocusOnFirstResponder(convertRawProp(
209+
+ context,
210+
+ rawProps,
211+
+ "preventFocusOnFirstResponder",
212+
+ sourceProps.preventFocusOnFirstResponder,
213+
+ {false})),
214+
disableKeyboardShortcuts(convertRawProp(
215+
context,
216+
rawProps,
217+
@@ -215,6 +221,7 @@ void BaseTextInputProps::setProp(
218+
RAW_SET_PROP_SWITCH_CASE_BASIC(submitBehavior);
219+
RAW_SET_PROP_SWITCH_CASE_BASIC(multiline);
220+
RAW_SET_PROP_SWITCH_CASE_BASIC(disableKeyboardShortcuts);
221+
+ RAW_SET_PROP_SWITCH_CASE_BASIC(preventFocusOnFirstResponder);
222+
}
223+
}
224+
225+
diff --git a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputProps.h b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputProps.h
226+
index 3e93402..092dbf0 100644
227+
--- a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputProps.h
228+
+++ b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputProps.h
229+
@@ -79,6 +79,8 @@ class BaseTextInputProps : public ViewProps, public BaseTextProps {
230+
231+
bool multiline{false};
232+
233+
+ bool preventFocusOnFirstResponder{false};
234+
+
235+
bool disableKeyboardShortcuts{false};
236+
};
237+

src/components/ActionSheetAwareScrollView/ActionSheetAwareScrollViewContext.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ const States = {
6767
POPOVER_CLOSED: 'popoverClosed',
6868
KEYBOARD_POPOVER_CLOSED: 'keyboardPopoverClosed',
6969
KEYBOARD_POPOVER_OPEN: 'keyboardPopoverOpen',
70-
KEYBOARD_CLOSED_POPOVER: 'keyboardClosingPopover',
70+
KEYBOARD_CLOSING_POPOVER: 'keyboardClosingPopover',
7171
POPOVER_MEASURED: 'popoverMeasured',
7272
MODAL_WITH_KEYBOARD_OPEN_DELETED: 'modalWithKeyboardOpenDeleted',
7373
} as const;
@@ -97,13 +97,13 @@ const STATE_MACHINE: StateMachine<ValueOf<typeof States>, ValueOf<typeof Actions
9797
},
9898
[States.KEYBOARD_POPOVER_OPEN]: {
9999
[Actions.MEASURE_POPOVER]: States.KEYBOARD_POPOVER_OPEN,
100-
[Actions.CLOSE_POPOVER]: States.KEYBOARD_CLOSED_POPOVER,
100+
[Actions.CLOSE_POPOVER]: States.KEYBOARD_CLOSING_POPOVER,
101101
[Actions.OPEN_KEYBOARD]: States.KEYBOARD_OPEN,
102102
},
103103
[States.KEYBOARD_POPOVER_CLOSED]: {
104104
[Actions.OPEN_KEYBOARD]: States.KEYBOARD_OPEN,
105105
},
106-
[States.KEYBOARD_CLOSED_POPOVER]: {
106+
[States.KEYBOARD_CLOSING_POPOVER]: {
107107
[Actions.OPEN_KEYBOARD]: States.KEYBOARD_OPEN,
108108
[Actions.END_TRANSITION]: States.KEYBOARD_OPEN,
109109
},

src/components/ActionSheetAwareScrollView/ActionSheetKeyboardSpace.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ function ActionSheetKeyboardSpace(props: ActionSheetKeyboardSpaceProps) {
136136
if (isClosedKeyboard || isOpeningKeyboard) {
137137
return lastKeyboardHeight - keyboardHeight;
138138
}
139-
if (previous.state === States.KEYBOARD_CLOSED_POPOVER || (previous.state === States.KEYBOARD_OPEN && elementOffset < 0)) {
139+
if (previous.state === States.KEYBOARD_CLOSING_POPOVER || (previous.state === States.KEYBOARD_OPEN && elementOffset < 0)) {
140140
const returnValue = Math.max(keyboard.heightWhenOpened.get() - keyboard.height.get() - paddingBottom, 0) + Math.max(elementOffset, 0);
141141
return returnValue;
142142
}
@@ -222,7 +222,7 @@ function ActionSheetKeyboardSpace(props: ActionSheetKeyboardSpaceProps) {
222222
return lastKeyboardHeight;
223223
}
224224

225-
case States.KEYBOARD_CLOSED_POPOVER: {
225+
case States.KEYBOARD_CLOSING_POPOVER: {
226226
if (elementOffset < 0) {
227227
transition({type: Actions.END_TRANSITION});
228228

0 commit comments

Comments
 (0)