Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,20 @@ export type NativeProps = $ReadOnly<{|
onSelectionChange?: ?DirectEventHandler<
$ReadOnly<{|
target: Int32,
selection: $ReadOnly<{|start: Double, end: Double|}>,
selection: $ReadOnly<{|
start: Double,
end: Double,
cursorPosition: $ReadOnly<{|
start: $ReadOnly<{|
x: Double,
y: Double,
|}>,
end: $ReadOnly<{|
x: Double,
y: Double,
|}>,
|}>,
|}>,
|}>,
>,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,16 @@ export interface TextInputSelectionChangeEventData extends TargetedEvent {
selection: {
start: number;
end: number;
cursorPosition: {
start: {
x: number;
y: number;
};
end: {
x: number;
y: number;
};
};
};
}

Expand Down Expand Up @@ -817,7 +827,28 @@ export interface TextInputProps
* The start and end of the text input's selection. Set start and end to
* the same value to position the cursor.
*/
selection?: {start: number; end?: number | undefined} | undefined;
selection?:
| {
start: number;
end?: number | undefined;
cursorPosition:
| {
start:
| {
x: number;
y: number;
}
| undefined;
end:
| {
x: number;
y: number;
}
| undefined;
}
| undefined;
}
| undefined;

/**
* The highlight (and cursor on ios) color of the text input
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@ export type FocusEvent = TargetEvent;
type Selection = $ReadOnly<{|
start: number,
end: number,
cursorPosition: $ReadOnly<{|
start: $ReadOnly<{|
x: number,
y: number,
|}>,
end: $ReadOnly<{|
x: number,
y: number,
|}>,
|}>,
|}>;

export type SelectionChangeEvent = SyntheticEvent<
Expand Down Expand Up @@ -837,6 +847,16 @@ export type Props = $ReadOnly<{|
selection?: ?$ReadOnly<{|
start: number,
end?: ?number,
cursorPosition: $ReadOnly<{|
start: $ReadOnly<{|
x: number,
y: number,
|}>,
end: $ReadOnly<{|
x: number,
y: number,
|}>,
|}>,
|}>,

/**
Expand Down
52 changes: 50 additions & 2 deletions packages/react-native/Libraries/Components/TextInput/TextInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,14 @@ type TextInputInstance = React.ElementRef<HostComponent<mixed>> & {
+clear: () => void,
+isFocused: () => boolean,
+getNativeRef: () => ?React.ElementRef<HostComponent<mixed>>,
+setSelection: (start: number, end: number) => void,
+setSelection: (
start: number,
end: number,
cursorPosition: {
start: {x: number, y: number},
end: {x: number, y: number},
},
) => void,
};

let AndroidTextInput;
Expand Down Expand Up @@ -79,6 +86,16 @@ export type TextInputEvent = SyntheticEvent<
range: $ReadOnly<{|
start: number,
end: number,
cursorPosition: $ReadOnly<{|
start: $ReadOnly<{|
x: number,
y: number,
|}>,
end: $ReadOnly<{|
x: number,
y: number,
|}>,
|}>,
|}>,
target: number,
text: string,
Expand Down Expand Up @@ -107,6 +124,16 @@ export type FocusEvent = TargetEvent;
type Selection = $ReadOnly<{|
start: number,
end: number,
cursorPosition: $ReadOnly<{|
start: $ReadOnly<{|
x: number,
y: number,
|}>,
end: $ReadOnly<{|
x: number,
y: number,
|}>,
|}>,
|}>;

export type SelectionChangeEvent = SyntheticEvent<
Expand Down Expand Up @@ -798,7 +825,7 @@ export type Props = $ReadOnly<{|
/**
* Callback that is called when the text input selection is changed.
* This will be called with
* `{ nativeEvent: { selection: { start, end } } }`.
* `{ nativeEvent: { selection: { start, end, cursorPosition: {start: {x, y}, end: {x, y}}} } }`.
*/
onSelectionChange?: ?(e: SelectionChangeEvent) => mixed,

Expand Down Expand Up @@ -875,10 +902,21 @@ export type Props = $ReadOnly<{|
/**
* The start and end of the text input's selection. Set start and end to
* the same value to position the cursor.
* cursorPosition specify the location of the cursor
*/
selection?: ?$ReadOnly<{|
start: number,
end?: ?number,
cursorPosition: $ReadOnly<{|
start: $ReadOnly<{|
x: number,
y: number,
|}>,
end: $ReadOnly<{|
x: number,
y: number,
|}>,
|}>,
|}>,

/**
Expand Down Expand Up @@ -1092,6 +1130,16 @@ function InternalTextInput(props: Props): React.Node {
: {
start: propsSelection.start,
end: propsSelection.end ?? propsSelection.start,
cursorPosition: {
start: {
x: propsSelection?.cursorPosition?.start?.x ?? 0,
y: propsSelection?.cursorPosition?.start?.y ?? 0,
},
end: {
x: propsSelection?.cursorPosition?.end?.x ?? 0,
y: propsSelection?.cursorPosition?.end?.y ?? 0,
},
},
};

const [mostRecentEventCount, setMostRecentEventCount] = useState<number>(0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.view.ViewTreeObserver;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.autofill.HintConstants;
Expand Down Expand Up @@ -1232,18 +1233,67 @@ public ReactSelectionWatcher(ReactEditText editText) {
@Override
public void onSelectionChanged(int start, int end) {
// Android will call us back for both the SELECTION_START span and SELECTION_END span in text
// To prevent double calling back into js we cache the result of the previous call and only
// To prevent double calling back into js, we cache the result of the previous call and only
// forward it on if we have new values

// Apparently Android might call this with an end value that is less than the start value
// Lets normalize them. See https://github.com/facebook/react-native/issues/18579
Layout layout = mReactEditText.getLayout();
if (layout == null) {
mReactEditText.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
mReactEditText.getViewTreeObserver().removeOnGlobalLayoutListener(this);
onSelectionChanged(start, end);
}
});
return;
}
int realStart = Math.min(start, end);
int realEnd = Math.max(start, end);
int cursorPositionStartX = 0;
int cursorPositionStartY = 0;
int cursorPositionEndX = 0;
int cursorPositionEndY = 0;

int lineStart = layout.getLineForOffset(realStart);
int baselineStart = layout.getLineBaseline(lineStart);
int ascentStart = layout.getLineAscent(lineStart);
cursorPositionStartX = (int) Math.round(PixelUtil.toDIPFromPixel(layout.getPrimaryHorizontal(realStart)));
cursorPositionStartY = (int) Math.round(PixelUtil.toDIPFromPixel(baselineStart + ascentStart));
int lineEnd = layout.getLineForOffset(realEnd);
int baselineEnd = layout.getLineBaseline(lineEnd);
int ascentEnd = layout.getLineAscent(lineEnd);
int descentEnd = layout.getLineDescent(lineEnd);

float right = layout.getPrimaryHorizontal(realEnd);
float bottom = layout.getLineBaseline(lineEnd) + layout.getLineDescent(lineEnd);
int cursorWidth = 0;

Drawable cursorDrawable = null;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
cursorDrawable = mReactEditText.getTextCursorDrawable();
if (cursorDrawable != null) {
cursorWidth = cursorDrawable.getIntrinsicWidth();
}
}

cursorPositionEndX = (int) Math.round(PixelUtil.toDIPFromPixel(right + cursorWidth));
cursorPositionEndY = (int) Math.round(PixelUtil.toDIPFromPixel(bottom));

if (mPreviousSelectionStart != realStart || mPreviousSelectionEnd != realEnd) {
mEventDispatcher.dispatchEvent(
new ReactTextInputSelectionEvent(
mSurfaceId, mReactEditText.getId(), realStart, realEnd));
new ReactTextInputSelectionEvent(
mSurfaceId,
mReactEditText.getId(),
realStart,
realEnd,
cursorPositionStartX,
cursorPositionStartY,
cursorPositionEndX,
cursorPositionEndY
)
);

mPreviousSelectionStart = realStart;
mPreviousSelectionEnd = realEnd;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,39 @@

private int mSelectionStart;
private int mSelectionEnd;
private int mCursorPositionStartX;
private int mCursorPositionStartY;
private int mCursorPositionEndX;
private int mCursorPositionEndY;

@Deprecated
public ReactTextInputSelectionEvent(int viewId, int selectionStart, int selectionEnd) {
this(ViewUtil.NO_SURFACE_ID, viewId, selectionStart, selectionEnd);
public ReactTextInputSelectionEvent(
int viewId,
int selectionStart,
int selectionEnd,
int cursorPositionStartX,
int cursorPositionStartY,
int cursorPositionEndX,
int cursorPositionEndY) {
this(-1, viewId, selectionStart, selectionEnd, cursorPositionStartX, cursorPositionStartY, cursorPositionEndX, cursorPositionEndY);
}

public ReactTextInputSelectionEvent(
int surfaceId, int viewId, int selectionStart, int selectionEnd) {
int surfaceId,
int viewId,
int selectionStart,
int selectionEnd,
int cursorPositionStartX,
int cursorPositionStartY,
int cursorPositionEndX,
int cursorPositionEndY) {
super(surfaceId, viewId);
mSelectionStart = selectionStart;
mSelectionEnd = selectionEnd;
mCursorPositionStartX = cursorPositionStartX;
mCursorPositionStartY = cursorPositionStartY;
mCursorPositionEndX = cursorPositionEndX;
mCursorPositionEndY = cursorPositionEndY;
}

@Override
Expand All @@ -42,10 +64,23 @@ public String getEventName() {
@Override
protected WritableMap getEventData() {
WritableMap eventData = Arguments.createMap();

WritableMap selectionData = Arguments.createMap();

WritableMap startPosition = Arguments.createMap();
startPosition.putInt("x", mCursorPositionStartX);
startPosition.putInt("y", mCursorPositionStartY);

WritableMap endPosition = Arguments.createMap();
endPosition.putInt("x", mCursorPositionEndX);
endPosition.putInt("y", mCursorPositionEndY);

WritableMap selectionPosition = Arguments.createMap();
selectionPosition.putMap("start", startPosition);
selectionPosition.putMap("end", endPosition);

selectionData.putInt("end", mSelectionEnd);
selectionData.putInt("start", mSelectionStart);
selectionData.putMap("cursorPosition", selectionPosition);

eventData.putMap("selection", selectionData);
return eventData;
Expand Down
Loading