Files
react-native/ReactAndroid/src/androidTest/java/com/facebook/react/tests/TextInputTestCase.java
T
Emily Janzer d4a498aba2 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
2020-02-18 12:14:17 -08:00

298 lines
12 KiB
Java

/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.react.tests;
import android.graphics.Color;
import android.text.style.ForegroundColorSpan;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.testing.ReactAppInstrumentationTestCase;
import com.facebook.react.testing.ReactInstanceSpecForTest;
import com.facebook.react.testing.StringRecordingModule;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.views.textinput.ReactEditText;
/** Test to verify that TextInput renders correctly */
public class TextInputTestCase extends ReactAppInstrumentationTestCase {
private final StringRecordingModule mRecordingModule = new StringRecordingModule();
private interface TextInputTestModule extends JavaScriptModule {
void setValueRef(String ref, String value);
}
/**
* Test that the actual height of the text input is not dependant on the font size of the text
* within.
*/
public void testTextInputMeasurements() {
View textInputViewHeightSet = getViewByTestId("textInput1");
EditText textInputViewNoHeight = getViewByTestId("textInput2");
int expectedHeight = Math.round(PixelUtil.toPixelFromDIP(30));
assertEquals(expectedHeight, textInputViewHeightSet.getHeight());
EditText editText = new EditText(textInputViewNoHeight.getContext());
editText.setTextSize(
TypedValue.COMPLEX_UNIT_PX, (float) Math.ceil(PixelUtil.toPixelFromSP(21.f)));
editText.setPadding(0, 0, 0, 0);
int measureSpec =
View.MeasureSpec.makeMeasureSpec(
ViewGroup.LayoutParams.WRAP_CONTENT, View.MeasureSpec.UNSPECIFIED);
editText.measure(measureSpec, measureSpec);
assertEquals(editText.getMeasuredHeight(), textInputViewNoHeight.getHeight());
}
/** Test that the cursor moves to the end of the word. */
public void testTextInputCursorPosition() throws Throwable {
final EditText textInputWithText = getViewByTestId("textInput3");
runTestOnUiThread(
new Runnable() {
@Override
public void run() {
textInputWithText.setSelection(3);
}
});
getReactContext()
.getJSModule(TextInputTestModule.class)
.setValueRef("textInput3", "Some other value");
waitForBridgeAndUIIdle();
assertEquals(4, textInputWithText.getSelectionStart());
assertEquals(4, textInputWithText.getSelectionEnd());
}
/** Test that the colors are applied to new text */
public void testTextInputColors() throws Throwable {
String testIDs[] = new String[] {"textInput4", "textInput5", "textInput6"};
for (String testID : testIDs) {
getReactContext().getJSModule(TextInputTestModule.class).setValueRef(testID, "NewText");
}
waitForBridgeAndUIIdle();
for (String testID : testIDs) {
ReactEditText reactEditText = getViewByTestId(testID);
assertEquals(
Color.GREEN,
reactEditText
.getText()
.getSpans(0, 1, ForegroundColorSpan.class)[0]
.getForegroundColor());
}
}
public void testOnSubmitEditing() throws Throwable {
String testId = "onSubmitTextInput";
ReactEditText reactEditText = getViewByTestId(testId);
fireEditorActionAndCheckRecording(reactEditText, EditorInfo.IME_ACTION_GO);
fireEditorActionAndCheckRecording(reactEditText, EditorInfo.IME_ACTION_DONE);
fireEditorActionAndCheckRecording(reactEditText, EditorInfo.IME_ACTION_NEXT);
fireEditorActionAndCheckRecording(reactEditText, EditorInfo.IME_ACTION_PREVIOUS);
fireEditorActionAndCheckRecording(reactEditText, EditorInfo.IME_ACTION_SEARCH);
fireEditorActionAndCheckRecording(reactEditText, EditorInfo.IME_ACTION_SEND);
fireEditorActionAndCheckRecording(reactEditText, EditorInfo.IME_ACTION_UNSPECIFIED);
fireEditorActionAndCheckRecording(reactEditText, EditorInfo.IME_ACTION_NONE);
}
public void testRequestFocusDoesNothing() throws Throwable {
String testId = "textInput1";
final ReactEditText reactEditText = getViewByTestId(testId);
runTestOnUiThread(
new Runnable() {
@Override
public void run() {
reactEditText.clearFocus();
}
});
waitForBridgeAndUIIdle();
assertFalse(reactEditText.isFocused());
runTestOnUiThread(
new Runnable() {
@Override
public void run() {
reactEditText.requestFocus();
}
});
waitForBridgeAndUIIdle();
// Calling requestFocus() directly should no-op
assertFalse(reactEditText.isFocused());
}
public void testRequestFocusFromJS() throws Throwable {
String testId = "textInput1";
final ReactEditText reactEditText = getViewByTestId(testId);
runTestOnUiThread(
new Runnable() {
@Override
public void run() {
reactEditText.clearFocus();
}
});
waitForBridgeAndUIIdle();
assertFalse(reactEditText.isFocused());
runTestOnUiThread(
new Runnable() {
@Override
public void run() {
reactEditText.requestFocusFromJS();
}
});
waitForBridgeAndUIIdle();
assertTrue(reactEditText.isFocused());
}
public void testAccessibilityFocus() throws Throwable {
String testId = "textInput1";
final ReactEditText reactEditText = getViewByTestId(testId);
runTestOnUiThread(
new Runnable() {
@Override
public void run() {
reactEditText.clearFocus();
}
});
waitForBridgeAndUIIdle();
assertFalse(reactEditText.isFocused());
runTestOnUiThread(
new Runnable() {
@Override
public void run() {
reactEditText.performAccessibilityAction(
AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
reactEditText.performAccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, null);
}
});
waitForBridgeAndUIIdle();
assertTrue(reactEditText.isFocused());
runTestOnUiThread(
new Runnable() {
@Override
public void run() {
reactEditText.performAccessibilityAction(
AccessibilityNodeInfo.ACTION_CLEAR_FOCUS, null);
}
});
waitForBridgeAndUIIdle();
assertFalse(reactEditText.isFocused());
}
private void fireEditorActionAndCheckRecording(
final ReactEditText reactEditText, final int actionId) throws Throwable {
fireEditorActionAndCheckRecording(reactEditText, actionId, true);
fireEditorActionAndCheckRecording(reactEditText, actionId, false);
}
private void fireEditorActionAndCheckRecording(
final ReactEditText reactEditText, final int actionId, final boolean blurOnSubmit)
throws Throwable {
mRecordingModule.reset();
runTestOnUiThread(
new Runnable() {
@Override
public void run() {
reactEditText.requestFocusFromJS();
reactEditText.setBlurOnSubmit(blurOnSubmit);
reactEditText.onEditorAction(actionId);
}
});
waitForBridgeAndUIIdle();
assertEquals(1, mRecordingModule.getCalls().size());
assertEquals(!blurOnSubmit, reactEditText.isFocused());
}
/**
* Test that the mentions input has colors displayed correctly. Removed for being flaky in open
* source, December 2016 public void testMetionsInputColors() throws Throwable { EventDispatcher
* eventDispatcher =
* getReactContext().getNativeModule(UIManagerModule.class).getEventDispatcher(); ReactEditText
* reactEditText = getViewByTestId("tokenizedInput"); String newText = "#Things and more #things";
* int contentWidth = reactEditText.getWidth(); int contentHeight = reactEditText.getHeight(); int
* start = 0; int count = newText.length();
*
* <p>eventDispatcher.dispatchEvent( new ReactTextChangedEvent( reactEditText.getId(),
* newText.toString(), (int) PixelUtil.toDIPFromPixel(contentWidth), (int)
* PixelUtil.toDIPFromPixel(contentHeight), reactEditText.incrementAndGetEventCounter()));
*
* <p>eventDispatcher.dispatchEvent( new ReactTextInputEvent( reactEditText.getId(),
* newText.toString(), "", start, start + count - 1)); waitForBridgeAndUIIdle();
*
* <p>ForegroundColorSpan[] spans = reactEditText .getText().getSpans(0,
* reactEditText.getText().length(), ForegroundColorSpan.class); assertEquals(2, spans.length);
* assertEquals(spans[0].getForegroundColor(), spans[1].getForegroundColor()); assertEquals(0,
* reactEditText.getText().getSpanStart(spans[1])); assertEquals(7,
* reactEditText.getText().getSpanEnd(spans[1])); assertEquals(newText.length() - 7,
* reactEditText.getText().getSpanStart(spans[0])); assertEquals(newText.length(),
* reactEditText.getText().getSpanEnd(spans[0]));
*
* <p>String moreText = "andsuch "; String previousText = newText; newText += moreText; count =
* moreText.length(); start = previousText.length();
*
* <p>eventDispatcher.dispatchEvent( new ReactTextChangedEvent( reactEditText.getId(),
* newText.toString(), (int) PixelUtil.toDIPFromPixel(contentWidth), (int)
* PixelUtil.toDIPFromPixel(contentHeight), reactEditText.incrementAndGetEventCounter()));
*
* <p>eventDispatcher.dispatchEvent( new ReactTextInputEvent( reactEditText.getId(), moreText, "",
* start, start + count - 1)); waitForBridgeAndUIIdle();
*
* <p>spans = reactEditText.getText() .getSpans(0, reactEditText.getText().length(),
* ForegroundColorSpan.class); assertEquals(2, spans.length);
* assertEquals(spans[0].getForegroundColor(), spans[1].getForegroundColor()); assertEquals(0,
* reactEditText.getText().getSpanStart(spans[1])); assertEquals(7,
* reactEditText.getText().getSpanEnd(spans[1])); assertEquals(newText.length() - 15,
* reactEditText.getText().getSpanStart(spans[0])); assertEquals(newText.length() - 1,
* reactEditText.getText().getSpanEnd(spans[0]));
*
* <p>moreText = "morethings"; previousText = newText; newText += moreText; count =
* moreText.length(); start = previousText.length();
*
* <p>eventDispatcher.dispatchEvent( new ReactTextChangedEvent( reactEditText.getId(),
* newText.toString(), (int) PixelUtil.toDIPFromPixel(contentWidth), (int)
* PixelUtil.toDIPFromPixel(contentHeight), reactEditText.incrementAndGetEventCounter()));
*
* <p>eventDispatcher.dispatchEvent( new ReactTextInputEvent( reactEditText.getId(), moreText, "",
* start, start + count - 1)); waitForBridgeAndUIIdle();
*
* <p>spans = reactEditText.getText() .getSpans(0, reactEditText.getText().length(),
* ForegroundColorSpan.class); assertEquals(spans[0].getForegroundColor(),
* spans[1].getForegroundColor()); assertEquals(2, spans.length); assertEquals(0,
* reactEditText.getText().getSpanStart(spans[1])); assertEquals(7,
* reactEditText.getText().getSpanEnd(spans[1])); assertEquals(newText.length() - 25,
* reactEditText.getText().getSpanStart(spans[0])); assertEquals(newText.length() - 11,
* reactEditText.getText().getSpanEnd(spans[0])); }
*/
@Override
protected ReactInstanceSpecForTest createReactInstanceSpecForTest() {
return super.createReactInstanceSpecForTest().addNativeModule(mRecordingModule);
}
@Override
protected String getReactApplicationKeyUnderTest() {
return "TextInputTestApp";
}
}