mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-10-03 00:27:30 -05:00
Move app to apps/web-react in pnpm workspace layout
This commit is contained in:
172
apps/web-react/app/form/fields/SelectFormField.tsx
Normal file
172
apps/web-react/app/form/fields/SelectFormField.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
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<V extends string> = Omit<
|
||||
FormFieldProps<"select">,
|
||||
"items" | "clearable" | "onBlur" | "name" | "searchable"
|
||||
> & {
|
||||
name?: string;
|
||||
items: FormFieldItems<V>;
|
||||
value: V | null;
|
||||
onChange: (value: V | null) => void;
|
||||
onSelect?: (value: V) => void;
|
||||
onBlur?: () => void;
|
||||
clearable?: boolean;
|
||||
searchable?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export function SelectFormField<V extends string>({
|
||||
name,
|
||||
label,
|
||||
bottomText,
|
||||
items,
|
||||
error,
|
||||
onBlur,
|
||||
value,
|
||||
onChange,
|
||||
onSelect,
|
||||
clearable,
|
||||
searchable,
|
||||
disabled,
|
||||
}: SelectFormFieldProps<V>) {
|
||||
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,
|
||||
};
|
||||
});
|
||||
|
||||
if (searchable) {
|
||||
return (
|
||||
<SearchableSelect
|
||||
name={name}
|
||||
label={label}
|
||||
bottomText={bottomText}
|
||||
error={error}
|
||||
items={itemsWithResolvedLabels}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
clearable={clearable}
|
||||
disabled={disabled}
|
||||
searchPlaceholder={t("common:actions.search")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const newValue = e.target.value === "" ? null : (e.target.value as V);
|
||||
onChange(newValue);
|
||||
if (newValue && onSelect) {
|
||||
onSelect(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FormFieldWrapper
|
||||
id={id}
|
||||
name={name}
|
||||
label={label}
|
||||
error={error}
|
||||
bottomText={bottomText}
|
||||
>
|
||||
<select
|
||||
id={id}
|
||||
name={name}
|
||||
value={value ?? ""}
|
||||
onChange={handleChange}
|
||||
onBlur={() => onBlur?.()}
|
||||
disabled={disabled}
|
||||
{...ariaAttributes({ name, error, bottomText })}
|
||||
>
|
||||
{clearable ? <option value="">—</option> : null}
|
||||
{itemsWithResolvedLabels.map((item) => (
|
||||
<option key={item.value} value={item.value}>
|
||||
{item.resolvedLabel}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</FormFieldWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
function SearchableSelect<V extends string>({
|
||||
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 }>;
|
||||
value: V | null;
|
||||
onChange: (value: V | null) => void;
|
||||
onBlur?: () => void;
|
||||
clearable?: boolean;
|
||||
disabled?: boolean;
|
||||
searchPlaceholder: string;
|
||||
}) {
|
||||
const { translatedLabel } = useTranslatedTexts({ label });
|
||||
|
||||
const selectItems = items.map((item) => ({
|
||||
id: item.value,
|
||||
textValue: item.resolvedLabel,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className={styles.searchable}>
|
||||
<SendouSelect
|
||||
label={translatedLabel}
|
||||
selectedKey={value}
|
||||
onSelectionChange={(key) => {
|
||||
const newValue = key === "" ? null : (key as V);
|
||||
onChange(newValue);
|
||||
onBlur?.();
|
||||
}}
|
||||
items={selectItems}
|
||||
search={{ placeholder: searchPlaceholder }}
|
||||
clearable={clearable}
|
||||
isDisabled={disabled}
|
||||
>
|
||||
{(item) => (
|
||||
<SendouSelectItem id={item.id} textValue={item.textValue}>
|
||||
{item.textValue}
|
||||
</SendouSelectItem>
|
||||
)}
|
||||
</SendouSelect>
|
||||
<FormFieldMessages name={name} error={error} bottomText={bottomText} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user