import * as React from "react"; import { useTranslation } from "react-i18next"; import { SendouSelect, SendouSelectItem } from "~/components/elements/Select"; import type { FormFieldItems, FormFieldProps } from "../types"; import { ariaAttributes } from "../utils"; import { FormFieldMessages, FormFieldWrapper, useTranslatedTexts, } from "./FormFieldWrapper"; import styles from "./SelectFormField.module.css"; type SelectFormFieldProps = Omit< FormFieldProps<"select">, "items" | "clearable" | "onBlur" | "name" | "searchable" > & { name?: string; items: FormFieldItems; value: V | null; onChange: (value: V | null) => void; onSelect?: (value: V) => void; onBlur?: () => void; clearable?: boolean; searchable?: boolean; disabled?: boolean; }; export function SelectFormField({ name, label, bottomText, items, error, onBlur, value, onChange, onSelect, clearable, searchable, disabled, }: SelectFormFieldProps) { const { t, i18n } = useTranslation(["common"]); const id = React.useId(); const itemsWithResolvedLabels = items.map((item) => { const itemLabel = item.label; const resolvedLabel = typeof itemLabel === "function" ? itemLabel(i18n.language) : typeof itemLabel === "string" && itemLabel.includes(":") && i18n.exists(itemLabel) ? t(itemLabel as never) : String(itemLabel); return { value: item.value, resolvedLabel, description: item.description, }; }); const hasDescriptions = itemsWithResolvedLabels.some( (item) => item.description, ); if (searchable || hasDescriptions) { return ( ); } const handleChange = (e: React.ChangeEvent) => { const newValue = e.target.value === "" ? null : (e.target.value as V); onChange(newValue); if (newValue && onSelect) { onSelect(newValue); } }; return ( ); } function CustomSelect({ name, label, bottomText, error, items, value, onChange, onBlur, clearable, disabled, searchPlaceholder, }: { name?: string; label?: string; bottomText?: string; error?: string; items: Array<{ value: V; resolvedLabel: string; description?: React.ReactNode; }>; value: V | null; onChange: (value: V | null) => void; onBlur?: () => void; clearable?: boolean; disabled?: boolean; searchPlaceholder?: string; }) { const { translatedLabel } = useTranslatedTexts({ label }); // the Autocomplete wrapper of searchable selects drops falsy keys, so only // plain selects render the clear choice as a list item like the native // select's "—" option; searchable ones keep the clear button const hasEmptyItem = Boolean(clearable && !searchPlaceholder); const selectItems = [ ...(hasEmptyItem ? [{ id: "", textValue: "—", description: undefined }] : []), ...items.map((item) => ({ id: item.value as string, textValue: item.resolvedLabel, description: item.description, })), ]; return (
{ const newValue = key === "" ? null : (key as V); onChange(newValue); onBlur?.(); }} items={selectItems} search={ searchPlaceholder ? { placeholder: searchPlaceholder } : undefined } clearable={clearable && !hasEmptyItem} isDisabled={disabled} > {(item) => ( {item.description ? ( {item.textValue} {item.description} ) : ( item.textValue )} )}
); }