kirillzyusko / react-native-keyboard-controller

Keyboard manager which works in identical way on both iOS and Android
https://kirillzyusko.github.io/react-native-keyboard-controller/
MIT License
1.62k stars 67 forks source link

The parameters returned by onSelectionChange are incorrect. #489

Closed zyslife closed 3 months ago

zyslife commented 3 months ago

Describe the bug When the TextInput is multiline and scrollEnabled={false}, and there is no height restriction, the start and end values are incorrect when a line break occurs. For more details, please refer to the screenshot and code example below.

Please replace the FocusedInputHandlers example.

Code snippet

import React, { useCallback, useEffect, useRef, useState } from "react";
import {
  StyleSheet,
  Text,
  TextInput,
  View,
  findNodeHandle,
} from "react-native";
import { useFocusedInputHandler } from "react-native-keyboard-controller";
import Reanimated, {
  runOnJS,
  useAnimatedStyle,
  useSharedValue,
} from "react-native-reanimated";

import type {
  TextInputProps,
  TextInputSelectionChangeEventData,
} from "react-native";
import type { TextInputMaskProps } from "react-native-text-input-mask";

type MaskedInputState = {
  formatted: string; // +1 (123) 456-78-90
  extracted?: string; // 1234567890
};

const TextInputWithMicSelection = (props: TextInputProps) => {
  const ref = useRef<TextInput>(null);
  const tag = useSharedValue(-1);
  const position = useSharedValue({ x: 0, y: 0 });

  useEffect(() => {
    tag.value = findNodeHandle(ref.current) ?? -1;
  }, []);

  useFocusedInputHandler(
    {
      onSelectionChange: (event) => {
        "worklet";

        if (event.target === tag.value) {
          position.value = {
            x: event.selection.end.x,
            y: event.selection.end.y,
          };
        }
      },
    },
    [],
  );

  const style = useAnimatedStyle(
    () => ({
      position: "absolute",
      width: 20,
      height: 20,
      backgroundColor: "blue",
      left: 10,
      transform: [
        {
          translateX: position.value.x,
        },
        {
          translateY: position.value.y,
        },
      ],
    }),
    [],
  );

  return (
    <View>
      <TextInput ref={ref} {...props} />
      <Reanimated.View style={style} />
    </View>
  );
};

export default function TextInputMaskExample() {
  const [data, setData] = useState<MaskedInputState>({
    formatted: "",
    extracted: "",
  });
  const [worklet, setWorkletData] = useState("");
  const [workletSelection, setWorkletSelection] = useState({
    target: -1,
    selection: {
      start: {
        x: 0,
        y: 0,
        position: 0,
      },
      end: {
        x: 0,
        y: 0,
        position: 0,
      },
    },
  });
  const [originalSelection, setOriginalSelection] =
    useState<TextInputSelectionChangeEventData | null>(null);

  useFocusedInputHandler(
    {
      onChangeText: ({ text }) => {
        "worklet";

        runOnJS(setWorkletData)(text);
      },
      onSelectionChange: (event) => {
        "worklet";

        runOnJS(setWorkletSelection)(event);
      },
    },
    [],
  );

  const onChangeText = useCallback<
    NonNullable<TextInputMaskProps["onChangeText"]>
  >((formatted, extracted) => {
    setData({ formatted, extracted });
  }, []);

  return (
    <View style={style.container}>
      <TextInput
        multiline
        onChangeText={onChangeText}
        onSelectionChange={({ nativeEvent }) =>
          setOriginalSelection(nativeEvent)
        }
        keyboardType="phone-pad"
        placeholder="+1 (___) ___ __ __"
        placeholderTextColor="gray"
        style={style.input}
        testID="masked_input"
        scrollEnabled={false}
      />
      <TextInputWithMicSelection
        onSelectionChange={({ nativeEvent }) =>
          setOriginalSelection(nativeEvent)
        }
        onChangeText={onChangeText}
        multiline
        style={style.input}
        testID="multiline_input"
      />
      <Text testID="formatted_text" style={style.text}>
        Formatted: {data.formatted}
      </Text>
      <Text testID="extracted_text" style={style.text}>
        Extracted: {data.extracted}
      </Text>
      <Text testID="worklet_text" style={style.text}>
        Worklet: {worklet}
      </Text>
      <Text testID="selection_text" style={[style.text, style.bold]}>
        Keyboard controller Selection:
      </Text>
      <Text testID="selection_text_start_end" style={style.text}>
        start: {workletSelection.selection.start.position}, end:{" "}
        {workletSelection.selection.end.position}
      </Text>
      <Text testID="selection_text_target" style={style.text}>
        target: {workletSelection.target}
      </Text>
      <Text testID="selection_text_coordinates_start" style={style.text}>
        startX: {Math.round(workletSelection.selection.start.x)}, startY:{" "}
        {Math.round(workletSelection.selection.start.y)}
      </Text>
      <Text testID="selection_text_coordinates_end" style={style.text}>
        endX: {Math.round(workletSelection.selection.end.x)}, endY:{" "}
        {Math.round(workletSelection.selection.end.y)}
      </Text>
      <Text testID="original_selection_text" style={[style.text, style.bold]}>
        Original selection:
      </Text>
      <Text testID="original_selection_text_start_end" style={style.text}>
        start: {originalSelection?.selection.start}, end:{" "}
        {originalSelection?.selection.end}
      </Text>
      <Text testID="original_selection_text_target" style={style.text}>
        target: {originalSelection?.target}
      </Text>
    </View>
  );
}

const style = StyleSheet.create({
  container: {
    marginHorizontal: 12,
  },
  input: {
    backgroundColor: "#dcdcdc",
    color: "black",
    borderRadius: 8,
    paddingHorizontal: 12,
    marginBottom: 12,
    marginVertical: 12,
  },
  text: {
    color: "black",
  },
  bold: {
    fontWeight: "bold",
  },
});

Screenshots ![Uploading Xnip2024-07-03_11-04-19.jpg…]()

Smartphone (please complete the following information):

zyslife commented 3 months ago

Xnip2024-07-03_11-04-19

kirillzyusko commented 3 months ago

Thanks @zyslife

I can reproduce the problem 👍

kirillzyusko commented 3 months ago

@zyslife can you check if https://github.com/kirillzyusko/react-native-keyboard-controller/pull/491 fixes the problem for you? 👀

zyslife commented 3 months ago

@zyslife can you check if #491 fixes the problem for you? 👀

Yes, it has been resolved. Thank you very much for your quick response. However, there is still an optimization point. Currently, when the cursor moves to a new line, the onSelectionChange of useFocusedInputHandler is called twice. The first time, it's startY: -1, endY: 1, and the second time, it's normal. Could it be called only once in a normal way?

kirillzyusko commented 3 months ago

Currently, when the cursor moves to a new line, the onSelectionChange of useFocusedInputHandler is called twice. The first time, it's startY: -1, endY: 1, and the second time, it's normal. Could it be called only once in a normal way?

@zyslife In my case it's called once and I don't see startY: -1, endY: 1 anymore. Here is logs from the app:

 LOG  {"eventName": "onFocusedInputSelectionChanged", "selection": {"end": {"position": 41, "x": 353.3333333333333, "y": 22.666666666666668}, "start": {"position": 41, "x": 351.3333333333333, "y": 4}}, "target": 513}
 LOG  {"eventName": "onFocusedInputSelectionChanged", "selection": {"end": {"position": 42, "x": 21.333333333333332, "y": 39.333333333333336}, "start": {"position": 42, "x": 19.333333333333332, "y": 20.666666666666668}}, "target": 513}
kirillzyusko commented 3 months ago

I've published 1.12.5 and I think the problem has been fixed there!

If not - let me know and I'll re-open this issue!

zyslife commented 2 months ago

I've published 1.12.5 and I think the problem has been fixed there!

If not - let me know and I'll re-open this issue!

Yes, I have verified in the latest version, and the issue has been resolved.