Skip to content

Commit d4a498a

Browse files
Emily Janzerfacebook-github-bot
Emily Janzer
authored andcommitted
Allow focusing TextInput when already focused
Summary: Right now, `requestFocus()` is a no-op if the EditText view thinks it's already focused. In certain cases, though, we still want to focus the view even if it's already focused - for example, if TalkBack is enabled and you dismiss the keyboard, you want to be able to tap on the TextInput again to bring back the keyboard, even though the View never thinks it lost focus. What I'm doing instead is basically disregarding the View's current focus state if we *would* focus the TextInput, which is in 3 circumstances: - When the view is attached to a window, if autofocus is true - When the focus is being requested by JS - When the focus is being requested by an accessibility action from the OS Changelog: [Android][Fixed] Change how TextInput responds to requestFocus to fix a11y focus issue Reviewed By: mdvacca Differential Revision: D19750312 fbshipit-source-id: 30b9fab40af4a083fa98f57aba7e586540238bea
1 parent d8ff5a5 commit d4a498a

File tree

2 files changed

+105
-24
lines changed

2 files changed

+105
-24
lines changed

ReactAndroid/src/androidTest/java/com/facebook/react/tests/TextInputTestCase.java

+92
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import android.util.TypedValue;
1313
import android.view.View;
1414
import android.view.ViewGroup;
15+
import android.view.accessibility.AccessibilityNodeInfo;
1516
import android.view.inputmethod.EditorInfo;
1617
import android.widget.EditText;
1718
import com.facebook.react.bridge.JavaScriptModule;
@@ -107,6 +108,97 @@ public void testOnSubmitEditing() throws Throwable {
107108
fireEditorActionAndCheckRecording(reactEditText, EditorInfo.IME_ACTION_NONE);
108109
}
109110

111+
public void testRequestFocusDoesNothing() throws Throwable {
112+
String testId = "textInput1";
113+
114+
final ReactEditText reactEditText = getViewByTestId(testId);
115+
runTestOnUiThread(
116+
new Runnable() {
117+
@Override
118+
public void run() {
119+
reactEditText.clearFocus();
120+
}
121+
});
122+
waitForBridgeAndUIIdle();
123+
assertFalse(reactEditText.isFocused());
124+
125+
runTestOnUiThread(
126+
new Runnable() {
127+
@Override
128+
public void run() {
129+
reactEditText.requestFocus();
130+
}
131+
});
132+
waitForBridgeAndUIIdle();
133+
134+
// Calling requestFocus() directly should no-op
135+
assertFalse(reactEditText.isFocused());
136+
}
137+
138+
public void testRequestFocusFromJS() throws Throwable {
139+
String testId = "textInput1";
140+
141+
final ReactEditText reactEditText = getViewByTestId(testId);
142+
143+
runTestOnUiThread(
144+
new Runnable() {
145+
@Override
146+
public void run() {
147+
reactEditText.clearFocus();
148+
}
149+
});
150+
waitForBridgeAndUIIdle();
151+
assertFalse(reactEditText.isFocused());
152+
153+
runTestOnUiThread(
154+
new Runnable() {
155+
@Override
156+
public void run() {
157+
reactEditText.requestFocusFromJS();
158+
}
159+
});
160+
waitForBridgeAndUIIdle();
161+
assertTrue(reactEditText.isFocused());
162+
}
163+
164+
public void testAccessibilityFocus() throws Throwable {
165+
String testId = "textInput1";
166+
167+
final ReactEditText reactEditText = getViewByTestId(testId);
168+
runTestOnUiThread(
169+
new Runnable() {
170+
@Override
171+
public void run() {
172+
reactEditText.clearFocus();
173+
}
174+
});
175+
waitForBridgeAndUIIdle();
176+
assertFalse(reactEditText.isFocused());
177+
178+
runTestOnUiThread(
179+
new Runnable() {
180+
@Override
181+
public void run() {
182+
reactEditText.performAccessibilityAction(
183+
AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
184+
reactEditText.performAccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, null);
185+
}
186+
});
187+
waitForBridgeAndUIIdle();
188+
assertTrue(reactEditText.isFocused());
189+
190+
runTestOnUiThread(
191+
new Runnable() {
192+
@Override
193+
public void run() {
194+
reactEditText.performAccessibilityAction(
195+
AccessibilityNodeInfo.ACTION_CLEAR_FOCUS, null);
196+
}
197+
});
198+
waitForBridgeAndUIIdle();
199+
assertFalse(reactEditText.isFocused());
200+
}
201+
110202
private void fireEditorActionAndCheckRecording(
111203
final ReactEditText reactEditText, final int actionId) throws Throwable {
112204
fireEditorActionAndCheckRecording(reactEditText, actionId, true);

ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java

+13-24
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,6 @@ public class ReactEditText extends AppCompatEditText {
6868
// *TextChanged events should be triggered. This is less expensive than removing the text
6969
// listeners and adding them back again after the text change is completed.
7070
protected boolean mIsSettingTextFromJS;
71-
// This component is controlled, so we want it to get focused only when JS ask it to do so.
72-
// Whenever android requests focus, except for accessibility click, it will be ignored.
73-
private boolean mShouldAllowFocus;
7471
private int mDefaultGravityHorizontal;
7572
private int mDefaultGravityVertical;
7673

@@ -127,7 +124,6 @@ public ReactEditText(Context context) {
127124
mNativeEventCount = 0;
128125
mMostRecentEventCount = 0;
129126
mIsSettingTextFromJS = false;
130-
mShouldAllowFocus = false;
131127
mBlurOnSubmit = null;
132128
mDisableFullscreen = false;
133129
mListeners = null;
@@ -152,10 +148,7 @@ public ReactEditText(Context context) {
152148
@Override
153149
public boolean performAccessibilityAction(View host, int action, Bundle args) {
154150
if (action == AccessibilityNodeInfo.ACTION_CLICK) {
155-
mShouldAllowFocus = true;
156-
requestFocus();
157-
mShouldAllowFocus = false;
158-
return true;
151+
return requestFocusInternal();
159152
}
160153
return super.performAccessibilityAction(host, action, args);
161154
}
@@ -248,18 +241,18 @@ public void clearFocus() {
248241

249242
@Override
250243
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
251-
// Always return true if we are already focused. This is used by android in certain places,
252-
// such as text selection.
253-
if (isFocused()) {
254-
return true;
255-
}
256-
257-
if (!mShouldAllowFocus) {
258-
return false;
259-
}
244+
// This is a no-op so that when the OS calls requestFocus(), nothing will happen. ReactEditText
245+
// is a controlled component, which means its focus is controlled by JS, with two exceptions:
246+
// autofocus when it's attached to the window, and responding to accessibility events. In both
247+
// of these cases, we call requestFocusInternal() directly.
248+
return isFocused();
249+
}
260250

251+
private boolean requestFocusInternal() {
261252
setFocusableInTouchMode(true);
262-
boolean focused = super.requestFocus(direction, previouslyFocusedRect);
253+
// We must explicitly call this method on the super class; if we call requestFocus() without
254+
// any arguments, it will call into the overridden requestFocus(int, Rect) above, which no-ops.
255+
boolean focused = super.requestFocus(View.FOCUS_DOWN, null);
263256
if (getShowSoftInputOnFocus()) {
264257
showSoftKeyboard();
265258
}
@@ -461,9 +454,7 @@ public void maybeUpdateTypeface() {
461454

462455
// VisibleForTesting from {@link TextInputEventsTestCase}.
463456
public void requestFocusFromJS() {
464-
mShouldAllowFocus = true;
465-
requestFocus();
466-
mShouldAllowFocus = false;
457+
requestFocusInternal();
467458
}
468459

469460
/* package */ void clearFocusFromJS() {
@@ -754,9 +745,7 @@ public void onAttachedToWindow() {
754745
}
755746

756747
if (mAutoFocus && !mDidAttachToWindow) {
757-
mShouldAllowFocus = true;
758-
requestFocus();
759-
mShouldAllowFocus = false;
748+
requestFocusInternal();
760749
}
761750

762751
mDidAttachToWindow = true;

0 commit comments

Comments
 (0)