|
1 | 1 | /* @flow strict-local */ |
2 | 2 | import React, { useState, useRef, useCallback, useContext } from 'react'; |
3 | 3 | import type { Node } from 'react'; |
4 | | -import { TextInput, TouchableWithoutFeedback, View } from 'react-native'; |
| 4 | +import { Platform, TextInput, TouchableWithoutFeedback, View, Keyboard } from 'react-native'; |
5 | 5 | import { useFocusEffect } from '@react-navigation/native'; |
6 | 6 | import type { ViewStyleProp } from 'react-native/Libraries/StyleSheet/StyleSheet'; |
7 | 7 |
|
@@ -55,6 +55,59 @@ type Props = $ReadOnly<{| |
55 | 55 | enablesReturnKeyAutomatically: boolean, |
56 | 56 | |}>; |
57 | 57 |
|
| 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 | + |
58 | 111 | export default function SmartUrlInput(props: Props): Node { |
59 | 112 | const { |
60 | 113 | defaultProtocol, |
@@ -113,19 +166,20 @@ export default function SmartUrlInput(props: Props): Node { |
113 | 166 | [defaultDomain, defaultProtocol, onChangeText], |
114 | 167 | ); |
115 | 168 |
|
| 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? |
116 | 174 | const urlPress = useCallback(() => { |
117 | 175 | if (textInputRef.current) { |
118 | 176 | // `.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(); |
126 | 178 | } |
127 | 179 | }, []); |
128 | 180 |
|
| 181 | + useRn19366Workaround(textInputRef); |
| 182 | + |
129 | 183 | const renderPlaceholderPart = (text: string) => ( |
130 | 184 | <TouchableWithoutFeedback onPress={urlPress}> |
131 | 185 | <ZulipText |
|
0 commit comments