Skip to content

Commit 7703751

Browse files
committed
SmartUrlInput: Stop jankily blurring then focusing when static text pressed
See https://chat.zulip.org/#narrow/stream/48-mobile/topic/can't.20paste.20org.20URL/near/1318357
1 parent 6b8a6b6 commit 7703751

File tree

1 file changed

+62
-8
lines changed

1 file changed

+62
-8
lines changed

src/common/SmartUrlInput.js

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* @flow strict-local */
22
import React, { useState, useRef, useCallback, useContext } from 'react';
33
import type { Node } from 'react';
4-
import { TextInput, TouchableWithoutFeedback, View } from 'react-native';
4+
import { Platform, TextInput, TouchableWithoutFeedback, View, Keyboard } from 'react-native';
55
import { useFocusEffect } from '@react-navigation/native';
66
import type { ViewStyleProp } from 'react-native/Libraries/StyleSheet/StyleSheet';
77

@@ -55,6 +55,59 @@ type Props = $ReadOnly<{|
5555
enablesReturnKeyAutomatically: boolean,
5656
|}>;
5757

58+
/**
59+
* Work around https://github.com/facebook/react-native/issues/19366.
60+
*
61+
* The bug: If the keyboard is dismissed only by pressing the built-in
62+
* Android back button, then the next time you call `.focus()` on the
63+
* input, the keyboard won't open again. On the other hand, if you call
64+
* `.blur()`, then the keyboard *will* open the next time you call
65+
* `.focus()`.
66+
*
67+
* This workaround: Call `.blur()` on the input whenever the keyboard is
68+
* closed, because it might have been closed by the built-in Android back
69+
* button. Then when we call `.focus()` the next time, it will open the
70+
* keyboard, as expected. (We only maintain that keyboard-closed listener
71+
* when this SmartUrlInput is on the screen that's focused in the
72+
* navigation.)
73+
*
74+
* Other workarounds that didn't work:
75+
* - When it comes time to do a `.focus()`, do a sneaky `.blur()` first,
76+
* then do the `.focus()` 100ms later. It's janky. This was #2078,
77+
* probably inspired by
78+
* https://github.com/facebook/react-native/issues/19366#issuecomment-400603928.
79+
* - Use RN's `BackHandler` to actually listen for the built-in Android back
80+
* button being used. That didn't work; the event handler wasn't firing
81+
* for either `backPress` or `hardwareBackPress` events. (We never
82+
* committed a version of this workaround.)
83+
*/
84+
function useRn19366Workaround(textInputRef) {
85+
if (Platform.OS !== 'android') {
86+
return;
87+
}
88+
89+
// (Disabling `react-hooks/rules-of-hooks` here is fine; the relevant rule
90+
// is not to call Hooks conditionally. But the platform conditional won't
91+
// vary in its behavior between multiple renders.)
92+
93+
// eslint-disable-next-line react-hooks/rules-of-hooks
94+
useFocusEffect(
95+
// eslint-disable-next-line react-hooks/rules-of-hooks
96+
React.useCallback(() => {
97+
const handleKeyboardDidHide = () => {
98+
if (textInputRef.current) {
99+
// `.current` is not type-checked; see definition.
100+
textInputRef.current.blur();
101+
}
102+
};
103+
104+
Keyboard.addListener('keyboardDidHide', handleKeyboardDidHide);
105+
106+
return () => Keyboard.removeListener('keyboardDidHide', handleKeyboardDidHide);
107+
}, [textInputRef]),
108+
);
109+
}
110+
58111
export default function SmartUrlInput(props: Props): Node {
59112
const {
60113
defaultProtocol,
@@ -113,19 +166,20 @@ export default function SmartUrlInput(props: Props): Node {
113166
[defaultDomain, defaultProtocol, onChangeText],
114167
);
115168

169+
// When the "placeholder parts" are pressed, i.e., the parts of the URL
170+
// line that aren't the TextInput itself, we still want to focus the
171+
// TextInput.
172+
// TODO(?): Is it a confusing UX to have a line that looks and acts like
173+
// a text input, but parts of it aren't really?
116174
const urlPress = useCallback(() => {
117175
if (textInputRef.current) {
118176
// `.current` is not type-checked; see definition.
119-
textInputRef.current.blur();
120-
setTimeout(() => {
121-
if (textInputRef.current) {
122-
// `.current` is not type-checked; see definition.
123-
textInputRef.current.focus();
124-
}
125-
}, 100);
177+
textInputRef.current.focus();
126178
}
127179
}, []);
128180

181+
useRn19366Workaround(textInputRef);
182+
129183
const renderPlaceholderPart = (text: string) => (
130184
<TouchableWithoutFeedback onPress={urlPress}>
131185
<ZulipText

0 commit comments

Comments
 (0)