96 lines
2.5 KiB
TypeScript
96 lines
2.5 KiB
TypeScript
import { useId } from "react";
|
|
import { t } from "@lingui/core/macro";
|
|
import {
|
|
Controller,
|
|
type FieldValues,
|
|
type Path,
|
|
type RegisterOptions,
|
|
useFormContext,
|
|
} from "react-hook-form";
|
|
|
|
import { TextEditor, type TextEditorHandle } from "../TextEditor.tsx";
|
|
|
|
interface RichTextFieldProps<T extends FieldValues> {
|
|
name: Path<T>;
|
|
label?: string;
|
|
placeholder?: string;
|
|
rows?: number;
|
|
/** Counted in the editor and enforced on submit, with a message saying so. */
|
|
maxLength?: number;
|
|
autoResize?: boolean;
|
|
disabled?: boolean;
|
|
rules?: RegisterOptions<T, Path<T>>;
|
|
/** Forwarded to the underlying TextEditor so callers can focus it. */
|
|
editorRef?: React.Ref<TextEditorHandle>;
|
|
/** Ctrl/Cmd+Enter handler (e.g. submit the comment). */
|
|
onSubmit?: () => void;
|
|
}
|
|
|
|
/**
|
|
* Rich-text field: a `Controller`-wrapped {@link TextEditor} (mentions, emoji,
|
|
* image upload, char count) bound to react-hook-form via the surrounding
|
|
* `FormProvider`.
|
|
*/
|
|
export function RichTextField<T extends FieldValues>({
|
|
name,
|
|
label,
|
|
placeholder,
|
|
rows,
|
|
maxLength,
|
|
autoResize,
|
|
disabled,
|
|
rules,
|
|
editorRef,
|
|
onSubmit,
|
|
}: RichTextFieldProps<T>) {
|
|
const id = useId();
|
|
const hintId = useId();
|
|
const { control, formState: { errors } } = useFormContext<T>();
|
|
const error = errors[name];
|
|
// A bare numeric rule fails without a message, which left a submit that did
|
|
// nothing and said nothing.
|
|
const allRules = maxLength == null ? rules : {
|
|
...rules,
|
|
maxLength: {
|
|
value: maxLength,
|
|
message: t`At most ${maxLength} characters.`,
|
|
},
|
|
};
|
|
|
|
return (
|
|
<div className="form-field">
|
|
{label && (
|
|
<label className="form-label" htmlFor={id}>
|
|
{label}
|
|
</label>
|
|
)}
|
|
<Controller
|
|
name={name}
|
|
control={control}
|
|
rules={allRules}
|
|
render={({ field }) => (
|
|
<TextEditor
|
|
ref={editorRef}
|
|
id={id}
|
|
value={field.value ?? ""}
|
|
onChange={field.onChange}
|
|
placeholder={placeholder}
|
|
rows={rows}
|
|
maxLength={maxLength}
|
|
autoResize={autoResize}
|
|
disabled={disabled}
|
|
onSubmit={onSubmit}
|
|
aria-invalid={error ? true : undefined}
|
|
aria-describedby={error?.message ? hintId : undefined}
|
|
/>
|
|
)}
|
|
/>
|
|
{error?.message && (
|
|
<span id={hintId} className="form-hint form-hint--error">
|
|
{String(error.message)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|