Files
pepperplus-ui/src/components/TextField.tsx
2026-01-28 10:34:36 +00:00

128 lines
4.2 KiB
TypeScript

// This program has been developed by students from the bachelor Computer Science at Utrecht
// University within the Software Project course.
// © Copyright Utrecht University (Department of Information and Computing Sciences)
import {useEffect, useState} from "react";
import styles from "./TextField.module.css";
/**
* A styled text input that updates its value **in real time** at every keystroke.
*
* Automatically toggles between read-only and editable modes to integrate with
* drag-based UIs (like React Flow). Calls `onCommit` when editing is completed.
*
* @param props - Component properties.
* @param props.value - The current text input value.
* @param props.setValue - Callback invoked on every keystroke to update the value.
* @param props.onCommit - Callback invoked when editing is finalized (on blur or Enter).
* @param props.placeholder - Optional placeholder text displayed when the input is empty.
* @param props.className - Optional additional CSS class names.
* @param props.id - Optional unique HTML `id` for the input element.
* @param props.ariaLabel - Optional ARIA label for accessibility.
* @param props.invalid - If true, applies error styling to indicate invalid input.
*
* @returns A styled `<input>` element that updates its value in real time.
*/
export function RealtimeTextField({
value = "",
setValue,
onCommit,
placeholder,
className,
id,
ariaLabel,
invalid = false,
} : {
value: string,
setValue: (value: string) => void,
onCommit: () => void,
placeholder?: string,
className?: string,
id?: string,
ariaLabel?: string,
invalid?: boolean,
}) {
/** Tracks whether the input is currently read-only (for drag compatibility). */
const [readOnly, setReadOnly] = useState(true);
/** Finalizes editing and calls `onCommit` when the user exits the field. */
const updateData = () => {
setReadOnly(true);
onCommit();
};
/** Handles the Enter key — commits the input by triggering a blur event. */
const updateOnEnter = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter")
(event.target as HTMLInputElement).blur(); };
return <input
type={"text"}
placeholder={placeholder}
value={value}
onChange={(e) => setValue(e.target.value)}
onFocus={() => setReadOnly(false)}
onBlur={updateData}
onKeyDown={updateOnEnter}
readOnly={readOnly}
id={id}
// ReactFlow uses the "drag" / "nodrag" classes to enable / disable dragging of nodes
className={`${readOnly ? "drag" : "nodrag"} flex-1 ${styles.textField} ${invalid ? styles.invalid : ""} ${className}`}
aria-label={ariaLabel}
/>;
}
/**
* A styled text input that updates its value **only on commit** (when the user
* presses Enter or clicks outside the input).
*
* Internally wraps `RealtimeTextField` and buffers input changes locally,
* calling `setValue` only once editing is complete.
*
* @param props - Component properties.
* @param props.value - The current text input value.
* @param props.setValue - Callback invoked when the user commits the change.
* @param props.placeholder - Optional placeholder text displayed when the input is empty.
* @param props.className - Optional additional CSS class names.
* @param props.id - Optional unique HTML `id` for the input element.
* @param props.ariaLabel - Optional ARIA label for accessibility.
* @param props.invalid - If true, applies error styling to indicate invalid input.
*
* @returns A styled `<input>` element that updates its parent state only on commit.
*/
export function TextField({
value = "",
setValue,
placeholder,
className,
id,
ariaLabel,
invalid = false,
} : {
value: string,
setValue: (value: string) => void,
placeholder?: string,
className?: string,
id?: string,
ariaLabel?: string,
invalid?: boolean,
}) {
const [inputValue, setInputValue] = useState(value);
useEffect(() => {
setInputValue(value);
}, [value]);
const onCommit = () => setValue(inputValue);
return <RealtimeTextField
placeholder={placeholder}
value={inputValue}
setValue={setInputValue}
onCommit={onCommit}
id={id}
className={className}
ariaLabel={ariaLabel}
invalid={invalid}
/>;
}