Files
sendou.ink/app/form/fields/FormFieldWrapper.tsx
2026-09-02 21:02:01 +03:00

133 lines
2.7 KiB
TypeScript

import type * as React from "react";
import { useTranslation } from "react-i18next";
import { FormMessage } from "~/components/FormMessage";
import { Label } from "~/components/Label";
import { errorMessageId, infoMessageId } from "../utils";
import styles from "./FormFieldWrapper.module.css";
export function useTranslatedTexts({
label,
error,
bottomText,
}: {
label?: string;
error?: string;
bottomText?: string;
}) {
const { t } = useTranslation(["forms"]);
return {
translatedLabel: label?.includes(":") ? t(label as never) : label,
translatedError: error?.includes(":") ? t(error as never) : error,
translatedBottomText: bottomText?.includes(":")
? t(bottomText as never)
: bottomText,
};
}
export function FormFieldMessages({
name,
error,
bottomText,
}: {
name?: string;
error?: string;
bottomText?: string;
}) {
const { translatedError, translatedBottomText } = useTranslatedTexts({
error,
bottomText,
});
return (
<>
{translatedError ? (
<FormMessage
type="error"
spaced={false}
id={name ? errorMessageId(name) : undefined}
>
{translatedError}
</FormMessage>
) : null}
{translatedBottomText ? (
<FormMessage
type="info"
spaced={false}
id={name ? infoMessageId(name) : undefined}
>
{translatedBottomText}
</FormMessage>
) : null}
</>
);
}
interface FormFieldWrapperProps {
id: string;
name?: string;
label?: string;
/** Extra element rendered next to the label, e.g. an `<InfoPopover />` explaining the field's syntax. */
labelPopover?: React.ReactNode;
required?: boolean;
error?: string;
bottomText?: string;
valueLimits?: { current: number; max: number };
children: React.ReactNode;
}
export function FormFieldWrapper({
id,
name,
label,
labelPopover,
required,
error,
bottomText,
valueLimits,
children,
}: FormFieldWrapperProps) {
const { translatedLabel } = useTranslatedTexts({ label });
const labelElement = translatedLabel ? (
<Label
htmlFor={id}
required={required}
valueLimits={valueLimits}
spaced={false}
>
{translatedLabel}
</Label>
) : null;
return (
<div className={styles.root}>
<div className="stack xs">
{labelElement && labelPopover ? (
<div className="stack horizontal xs items-center">
{labelElement}
{labelPopover}
</div>
) : (
labelElement
)}
{children}
<FormFieldMessages name={name} error={error} bottomText={bottomText} />
</div>
</div>
);
}
/** For the entity-search fields, which render their own label through the search component. */
export function SearchFormFieldWrapper({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className={styles.searchRoot}>
<div className="stack xs">{children}</div>
</div>
);
}