src/components/dives/dive-vocabulary-field.tsx
Rama main · réplica de lectura del repositorio original.
"use client";
import { Control, FieldValues, Path } from "react-hook-form";
import type { LucideIcon } from "lucide-react";
import { labelOrFallback } from "@/lib/api/labels";
import { NativeSelect } from "@/components/ui/native-select";
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
export interface DiveVocabularyFieldProps<
TFieldValues extends FieldValues,
TValue extends string,
> {
control: Control<TFieldValues>;
name: Path<TFieldValues>;
label: string;
icon: LucideIcon;
/**
* What the picker offers, in order: the API's vocabulary or part of it. A stored
* value outside it is still offered while the field holds it, so a save never
* drops it unasked.
*/
values: readonly TValue[];
labels: Record<TValue, string>;
}
// One of the dive's closed vocabularies - the water, the dive type, the
// conditions, the entry - as a select with "Not recorded" first.
//
// The three states behind "Not recorded" are DECISIONS.md's "A dive-level select
// carries the same three states".
export function DiveVocabularyField<
TFieldValues extends FieldValues,
TValue extends string,
>({
control,
name,
label,
icon: Icon,
values,
labels,
}: DiveVocabularyFieldProps<TFieldValues, TValue>) {
return (
<FormField
control={control}
name={name}
render={({ field }) => (
<FormItem>
<FormLabel>{label}</FormLabel>
<div className="relative">
<Icon className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2" />
<FormControl
render={
<NativeSelect
className="pl-9"
{...field}
value={field.value ?? ""}
// `""` straight through, not `|| undefined`: react-hook-form
// re-displays a field's default whenever its value resolves to
// `undefined`, so mapping "Not recorded" to it would snap the
// dive's stored value back the moment it was cleared. The submit
// paths convert the sentinel away.
onChange={(e) => field.onChange(e.target.value)}
/>
}
>
<option value="">Not recorded</option>
{(!field.value || values.includes(field.value)
? values
: [...values, field.value as TValue]
).map((value) => (
<option key={value} value={value}>
{labelOrFallback(labels, value)}
</option>
))}
</FormControl>
</div>
<FormMessage />
</FormItem>
)}
/>
);
}