fix: add cursor state to not skip to end on input components (#9375)

* Add cursor handling to input component

* add cursor handling to text area component

* [autofix.ci] apply automated fixes

* Modified tests to check cursor position

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Lucas Oliveira 2025-08-13 12:54:49 -03:00 • committed by GitHub
commit 181606fd80
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 72 additions and 4 deletions

View file

@ -1,7 +1,7 @@
import { PopoverAnchor } from "@radix-ui/react-popover"; import { PopoverAnchor } from "@radix-ui/react-popover";
import { uniqueId } from "lodash";
import { X } from "lucide-react"; import { X } from "lucide-react";
import { type ReactNode, useMemo, useState } from "react"; import { type ReactNode, useEffect, useMemo, useState } from "react";
import ForwardedIconComponent from "@/components/common/genericIconComponent"; import ForwardedIconComponent from "@/components/common/genericIconComponent";
import ShadTooltip from "@/components/common/shadTooltipComponent"; import ShadTooltip from "@/components/common/shadTooltipComponent";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@ -189,12 +189,20 @@ const CustomInputPopover = ({
hasRefreshButton, hasRefreshButton,
}) => { }) => {
const [isFocused, setIsFocused] = useState(false); const [isFocused, setIsFocused] = useState(false);
const [cursor, setCursor] = useState<number | null>(null);
const memoizedOptions = useMemo(() => new Set<string>(options), [options]); const memoizedOptions = useMemo(() => new Set<string>(options), [options]);
const PopoverContentInput = editNode const PopoverContentInput = editNode
? PopoverContent ? PopoverContent
: PopoverContentWithoutPortal; : PopoverContentWithoutPortal;
// Restore cursor position after value changes
useEffect(() => {
if (cursor !== null && refInput.current) {
refInput.current.setSelectionRange(cursor, cursor);
}
}, [cursor, value]);
const handleRemoveOption = ( const handleRemoveOption = (
optionToRemove: string, optionToRemove: string,
e: React.MouseEvent<HTMLButtonElement>, e: React.MouseEvent<HTMLButtonElement>,
@ -270,7 +278,7 @@ const CustomInputPopover = ({
autoComplete="off" autoComplete="off"
onFocus={() => setIsFocused(true)} onFocus={() => setIsFocused(true)}
autoFocus={autoFocus} autoFocus={autoFocus}
id={id + uniqueId()} id={id}
ref={refInput} ref={refInput}
type={!pwdVisible && password ? "password" : "text"} type={!pwdVisible && password ? "password" : "text"}
onBlur={() => { onBlur={() => {
@ -292,7 +300,10 @@ const CustomInputPopover = ({
? "" ? ""
: placeholder : placeholder
} }
onChange={(e) => onChange?.(e.target.value)} onChange={(e) => {
setCursor(e.target.selectionStart);
onChange?.(e.target.value);
}}
onKeyDown={(e) => { onKeyDown={(e) => {
handleKeyDown?.(e); handleKeyDown?.(e);
if (blurOnEnter && e.key === "Enter") refInput.current?.blur(); if (blurOnEnter && e.key === "Enter") refInput.current?.blur();

View file

@ -1,4 +1,5 @@
import { PopoverAnchor } from "@radix-ui/react-popover"; import { PopoverAnchor } from "@radix-ui/react-popover";
import { useEffect, useState } from "react";
import ForwardedIconComponent from "@/components/common/genericIconComponent"; import ForwardedIconComponent from "@/components/common/genericIconComponent";
import { import {
Command, Command,
@ -40,11 +41,21 @@ const CustomInputPopoverObject = ({
handleKeyDown, handleKeyDown,
showOptions, showOptions,
}) => { }) => {
const [cursor, setCursor] = useState<number | null>(null);
const PopoverContentInput = editNode const PopoverContentInput = editNode
? PopoverContent ? PopoverContent
: PopoverContentWithoutPortal; : PopoverContentWithoutPortal;
// Restore cursor position after value changes
useEffect(() => {
if (cursor !== null && refInput.current) {
refInput.current.setSelectionRange(cursor, cursor);
}
}, [cursor, value]);
const handleInputChange = (e) => { const handleInputChange = (e) => {
setCursor(e.target.selectionStart);
onChange && onChange(e.target.value); onChange && onChange(e.target.value);
}; };

View file

@ -45,6 +45,7 @@ export default function InputComponent({
hasRefreshButton = false, hasRefreshButton = false,
}: InputComponentType): JSX.Element { }: InputComponentType): JSX.Element {
const [pwdVisible, setPwdVisible] = useState(false); const [pwdVisible, setPwdVisible] = useState(false);
const [cursor, setCursor] = useState<number | null>(null);
const refInput = useRef<HTMLInputElement>(null); const refInput = useRef<HTMLInputElement>(null);
const [showOptions, setShowOptions] = useState<boolean>(false); const [showOptions, setShowOptions] = useState<boolean>(false);
@ -54,6 +55,13 @@ export default function InputComponent({
} }
}, [disabled]); }, [disabled]);
// Restore cursor position after value changes
useEffect(() => {
if (cursor !== null && refInput.current) {
refInput.current.setSelectionRange(cursor, cursor);
}
}, [cursor, value]);
function onInputLostFocus(event): void { function onInputLostFocus(event): void {
if (onBlur) onBlur(event); if (onBlur) onBlur(event);
} }
@ -83,6 +91,7 @@ export default function InputComponent({
)} )}
placeholder={password && editNode ? "Key" : placeholder} placeholder={password && editNode ? "Key" : placeholder}
onChange={(e) => { onChange={(e) => {
setCursor(e.target.selectionStart);
if (onChangeFolderName) { if (onChangeFolderName) {
return onChangeFolderName(e); return onChangeFolderName(e);
} }

View file

@ -74,6 +74,7 @@ export default function TextAreaComponent({
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const [isFocused, setIsFocused] = useState(false); const [isFocused, setIsFocused] = useState(false);
const [passwordVisible, setPasswordVisible] = useState(false); const [passwordVisible, setPasswordVisible] = useState(false);
const [cursor, setCursor] = useState<number | null>(null);
const isWebhook = useMemo( const isWebhook = useMemo(
() => nodeInformationMetadata?.nodeType === "webhook", () => nodeInformationMetadata?.nodeType === "webhook",
@ -100,6 +101,13 @@ export default function TextAreaComponent({
} }
}, [isWebhook, value, nodeInformationMetadata, handleOnNewValue]); }, [isWebhook, value, nodeInformationMetadata, handleOnNewValue]);
// Restore cursor position after value changes
useEffect(() => {
if (cursor !== null && inputRef.current) {
inputRef.current.setSelectionRange(cursor, cursor);
}
}, [cursor, value]);
const getInputClassName = () => { const getInputClassName = () => {
return cn( return cn(
inputClasses.base({ isFocused, password: password! }), inputClasses.base({ isFocused, password: password! }),
@ -111,6 +119,7 @@ export default function TextAreaComponent({
}; };
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setCursor(e.target.selectionStart);
handleOnNewValue({ value: e.target.value }); handleOnNewValue({ value: e.target.value });
}; };

View file

@ -38,6 +38,19 @@ test(
expect(false).toBeTruthy(); expect(false).toBeTruthy();
} }
// Test cursor position preservation
const input = page.getByTestId("popover-anchor-input-collection_name");
await input.click();
await input.press("Home"); // Move cursor to start
await input.press("ArrowRight"); // Move cursor to position 1
await input.press("ArrowRight"); // Move cursor to position 2
await input.pressSequentially("X", { delay: 100 }); // Type at position 2
const cursorValue = await input.inputValue();
if (!cursorValue.startsWith("coX")) {
expect(false).toBeTruthy();
}
await input.fill("collection_name_test_123123123!@#$&*(&%$@");
await page.getByTestId("div-generic-node").click(); await page.getByTestId("div-generic-node").click();
await page.getByTestId("edit-button-modal").last().click(); await page.getByTestId("edit-button-modal").last().click();

View file

@ -44,6 +44,21 @@ test(
"test test test test test test test test test test test !@#%*)( 123456789101010101010101111111111 !!!!!!!!!!", "test test test test test test test test test test test !@#%*)( 123456789101010101010101111111111 !!!!!!!!!!",
); );
// Test cursor position preservation
const textInput = page.getByTestId("textarea_str_text");
await textInput.click();
await textInput.press("Home"); // Move cursor to start
await textInput.press("ArrowRight"); // Move cursor to position 1
await textInput.press("ArrowRight"); // Move cursor to position 2
await textInput.pressSequentially("Y", { delay: 100 }); // Type at position 2
const cursorValue = await textInput.inputValue();
if (!cursorValue.startsWith("teY")) {
expect(false).toBeTruthy();
}
await textInput.fill(
"test test test test test test test test test test test !@#%*)( 123456789101010101010101111111111 !!!!!!!!!!",
);
await page await page
.getByTestId("button_open_text_area_modal_textarea_str_text") .getByTestId("button_open_text_area_modal_textarea_str_text")
.click(); .click();