sendou.ink/app/form/fields/InputGroupFormField.tsx
2026-01-18 18:21:19 +02:00

165 lines
3.7 KiB
TypeScript

import * as React from "react";
import { useTranslation } from "react-i18next";
import { Image } from "~/components/Image";
import type { FormFieldItemsWithImage, FormFieldProps } from "../types";
import { FormFieldWrapper } from "./FormFieldWrapper";
type RadioGroupFormFieldProps<V extends string> = Omit<
FormFieldProps<"radio-group">,
"items"
> & {
items: FormFieldItemsWithImage<V>;
value: V;
onChange: (value: V) => void;
};
export function RadioGroupFormField<V extends string>({
name,
label,
bottomText,
items,
error,
onBlur,
value,
onChange,
minLength,
}: RadioGroupFormFieldProps<V>) {
const id = React.useId();
const itemsWithLabels = useItemsWithResolvedLabels(items);
const required = typeof minLength !== "number" || minLength > 0;
return (
<FormFieldWrapper
id={id}
name={name}
label={label}
required={required}
error={error}
bottomText={bottomText}
>
<div
role="radiogroup"
aria-orientation="vertical"
aria-labelledby={id}
className="stack sm items-start"
>
{itemsWithLabels.map((item) => (
<div key={item.value} className="stack horizontal sm items-center">
<input
type="radio"
id={`${id}-${item.value}`}
name={id}
value={item.value}
checked={value === item.value}
onChange={() => onChange(item.value)}
onBlur={() => onBlur?.()}
/>
<label
htmlFor={`${id}-${item.value}`}
className="stack horizontal sm items-center mb-0 whitespace-nowrap"
>
{item.imgSrc ? (
<Image path={item.imgSrc} width={24} height={24} alt="" />
) : null}
{item.resolvedLabel}
</label>
</div>
))}
</div>
</FormFieldWrapper>
);
}
type CheckboxGroupFormFieldProps<V extends string> = Omit<
FormFieldProps<"checkbox-group">,
"items"
> & {
items: FormFieldItemsWithImage<V>;
value: V[];
onChange: (value: V[]) => void;
};
export function CheckboxGroupFormField<V extends string>({
name,
label,
bottomText,
items,
error,
onBlur,
value,
onChange,
minLength,
}: CheckboxGroupFormFieldProps<V>) {
const id = React.useId();
const itemsWithLabels = useItemsWithResolvedLabels(items);
const required = typeof minLength !== "number" || minLength > 0;
const handleChange = (itemValue: V, checked: boolean) => {
if (checked) {
onChange([...value, itemValue]);
} else {
onChange(value.filter((v) => v !== itemValue));
}
};
return (
<FormFieldWrapper
id={id}
name={name}
label={label}
required={required}
error={error}
bottomText={bottomText}
>
<div className="stack sm items-start">
{itemsWithLabels.map((item) => (
<div key={item.value} className="stack horizontal sm items-center">
<input
type="checkbox"
id={`${id}-${item.value}`}
value={item.value}
checked={value.includes(item.value)}
onChange={(e) => handleChange(item.value, e.target.checked)}
onClick={() => onBlur?.()}
/>
<label
htmlFor={`${id}-${item.value}`}
className="stack horizontal sm items-center mb-0 whitespace-nowrap"
>
{item.imgSrc ? (
<Image path={item.imgSrc} width={24} height={24} alt="" />
) : null}
{item.resolvedLabel}
</label>
</div>
))}
</div>
</FormFieldWrapper>
);
}
function useItemsWithResolvedLabels<V extends string>(
items: FormFieldItemsWithImage<V>,
) {
const { t, i18n } = useTranslation();
return items.map((item) => {
const itemLabel = item.label;
const resolvedLabel =
typeof itemLabel === "function"
? itemLabel(i18n.language)
: typeof itemLabel === "string" && itemLabel.includes(":")
? t(itemLabel as never)
: String(itemLabel);
return {
...item,
resolvedLabel,
};
});
}