openhands / src /components /features /settings /settings-input.tsx
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
9b906ea verified
Raw
History Blame Contribute Delete
4.38 kB
import { forwardRef } from "react";
import { cn } from "#/utils/utils";
import { formControlSettingsFieldClassName } from "#/utils/form-control-classes";
import { OptionalTag } from "./optional-tag";
interface SettingsInputProps {
testId?: string;
name?: string;
label: string;
type: React.HTMLInputTypeAttribute;
defaultValue?: string;
value?: string;
placeholder?: string;
showOptionalTag?: boolean;
isDisabled?: boolean;
startContent?: React.ReactNode;
className?: string;
onChange?: (value: string) => void;
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
required?: boolean;
min?: number;
max?: number;
step?: number;
pattern?: string;
/** Validation message shown when pattern doesn't match */
title?: string;
labelClassName?: string;
/**
* The input's accessible name, for the caller that renders the visible label
* itself. Only for those: a field labelled by this component reads its label,
* and two names would disagree.
*/
ariaLabel?: string;
/** ARIA describedby attribute for accessibility */
ariaDescribedBy?: string;
/** ARIA invalid attribute for accessibility */
ariaInvalid?: boolean;
/**
* Validation error message. When set, the input gets a red border and
* the message is rendered below it. Also sets aria-invalid automatically.
*/
error?: string;
/** Renders a red asterisk next to the label to mark the field as required. */
showRequiredTag?: boolean;
/**
* Short guidance rendered next to the label, above the input. It sits inside
* the `<label>`, so it is announced as part of the field's accessible name —
* keep it to a phrase that reads well after the label text.
*/
hint?: string;
onBlur?: () => void;
/** Extra classes merged onto the `<input>` element. */
inputClassName?: string;
}
export const SettingsInput = forwardRef<HTMLInputElement, SettingsInputProps>(
function SettingsInput(
{
testId,
name,
label,
type,
defaultValue,
value,
placeholder,
showOptionalTag,
isDisabled,
startContent,
className,
onChange,
onKeyDown,
required,
min,
max,
step,
pattern,
title,
labelClassName,
ariaLabel,
ariaDescribedBy,
ariaInvalid,
error,
showRequiredTag,
hint,
onBlur,
inputClassName,
},
ref,
) {
const errorId = error && testId ? `${testId}-error` : undefined;
return (
<label className={cn("flex flex-col gap-2.5 w-full min-w-0", className)}>
<div className="flex items-center gap-2">
{startContent}
<span className={cn("text-sm", labelClassName)}>{label}</span>
{showRequiredTag && (
<span className="text-red-400 text-sm leading-none" aria-hidden>
*
</span>
)}
{showOptionalTag && <OptionalTag />}
{hint && (
<span
data-testid={testId ? `${testId}-hint` : undefined}
className="min-w-0 text-xs text-[var(--oh-muted)]"
>
{hint}
</span>
)}
</div>
<input
ref={ref}
data-testid={testId}
onChange={(e) => onChange?.(e.target.value)}
onKeyDown={onKeyDown}
onBlur={onBlur}
name={name}
disabled={isDisabled}
type={type}
defaultValue={defaultValue}
value={value}
placeholder={placeholder}
min={min}
max={max}
step={step}
required={required}
pattern={pattern}
title={title}
aria-label={ariaLabel}
aria-describedby={errorId ?? ariaDescribedBy}
aria-invalid={!!error || ariaInvalid}
className={cn(
formControlSettingsFieldClassName,
"disabled:bg-[var(--oh-surface-raised)] disabled:border-[var(--oh-border-subtle)]",
error && "border-red-500",
inputClassName,
)}
/>
{error && (
<p
id={errorId}
role="alert"
data-testid={testId ? `${testId}-error` : undefined}
className="text-xs text-red-400 -mt-1"
>
{error}
</p>
)}
</label>
);
},
);