Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { forwardRef, useEffect, useRef, useState } from "react";
import { Input } from "@/components/ui/input";
import { handleKeyDown } from "@/utils/reactflowUtils";
import { cn } from "@/utils/utils";

interface CursorInputProps {
value: string;
onChange: (value: string) => void;
disabled?: boolean;
placeholder?: string;
className?: string;
dataTestId?: string;
editNode?: boolean;
onFocus?: () => void;
onBlur?: () => void;
}

export const CursorInput = forwardRef<HTMLInputElement, CursorInputProps>(
(
{
value,
onChange,
disabled = false,
placeholder,
className,
dataTestId,
editNode = false,
onFocus,
onBlur,
},
ref,
) => {
// Local state for input value to handle cursor position
const [localValue, setLocalValue] = useState<string>(value);
const [cursor, setCursor] = useState<number | null>(null);
const inputRef = useRef<HTMLInputElement>(null);

// Update local value when prop changes
useEffect(() => {
setLocalValue(value);
}, [value]);

// Handle cursor position restoration
useEffect(() => {
if (inputRef.current && cursor !== null) {
inputRef.current.setSelectionRange(cursor, cursor);
}
}, [cursor]);

const handleChangeInput = (e: React.ChangeEvent<HTMLInputElement>) => {
setCursor(e.target.selectionStart);
setLocalValue(e.target.value);
onChange(e.target.value);
};

const handleInputBlur = () => {
onBlur?.();
};

const handleInputFocus = () => {
onFocus?.();
};

return (
<Input
ref={ref || inputRef}
disabled={disabled}
type="text"
value={localValue}
className={cn(
"w-full text-primary",
editNode ? "input-edit-node" : "",
disabled ? "disabled-state" : "",
className,
)}
placeholder={placeholder}
onChange={handleChangeInput}
onKeyDown={(event) => handleKeyDown(event, localValue, "")}
data-testid={dataTestId}
onFocus={handleInputFocus}
onBlur={handleInputBlur}
/>
);
},
);

CursorInput.displayName = "CursorInput";
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { useCallback, useEffect, useRef, useState } from "react";

import { Button } from "@/components/ui/button";
import { cn } from "../../../../../utils/utils";
import { Input } from "../../../../ui/input";
import { getPlaceholder } from "../../helpers/get-placeholder-disabled";
import type { InputListComponentType, InputProps } from "../../types";
import { ButtonInputList } from "./components/button-input-list";
import { CursorInput } from "./components/cursor-input";
import { DeleteButtonInputList } from "./components/delete-button-input-list";

export default function InputListComponent({
Expand Down Expand Up @@ -92,22 +92,15 @@ export default function InputListComponent({
{value.map((singleValue, index) => (
<div key={index} className="flex w-full items-center">
<div className="group relative flex-1">
<Input
<CursorInput
ref={index === 0 ? inputRef : null}
disabled={disabled}
type="text"
value={singleValue}
className={cn(
"w-full text-primary",
value.length > 1 && "pr-10",
editNode ? "input-edit-node" : "",
disabled ? "disabled-state" : "",
)}
className={cn(value.length > 1 && "pr-10")}
placeholder={getPlaceholder(disabled, placeholder)}
onChange={(event) =>
handleInputChange(index, event.target.value)
}
data-testid={`${id}_${index}`}
onChange={(newValue) => handleInputChange(index, newValue)}
dataTestId={`${id}_${index}`}
editNode={editNode}
onFocus={() => setFocusedIndex(index)}
onBlur={() => setFocusedIndex(null)}
/>
Expand Down
15 changes: 15 additions & 0 deletions src/frontend/tests/core/unit/inputListComponent.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,21 @@ test(

await page.getByTestId("inputlist_str_urls_0").fill("test test test test");

// Test cursor position preservation
const input = page.getByTestId("inputlist_str_urls_0");
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("XD", { delay: 100 }); // Type at position 2

const cursorValue = await input.inputValue();
if (!cursorValue.startsWith("teXD")) {
expect(false).toBeTruthy();
}

await page.getByTestId("inputlist_str_urls_0").fill("test test test test");

await page.getByTestId("input-list-plus-btn_urls-0").click();

await page.getByTestId("input-list-plus-btn_urls-0").click();
Expand Down
Loading