96 lines
3.2 KiB
TypeScript
96 lines
3.2 KiB
TypeScript
import { useState, type KeyboardEvent } from 'react';
|
||
import { Controller, useFormContext } from 'react-hook-form';
|
||
import { FieldError } from './FieldError';
|
||
|
||
type Props = {
|
||
name: string;
|
||
label: string;
|
||
hint?: string;
|
||
placeholder?: string;
|
||
};
|
||
|
||
export function ChipsField({ name, label, hint, placeholder = 'Ajouter puis Entrée…' }: Props) {
|
||
const { control } = useFormContext();
|
||
const [draft, setDraft] = useState('');
|
||
|
||
return (
|
||
<Controller
|
||
name={name}
|
||
control={control}
|
||
render={({ field }) => {
|
||
const items: string[] = Array.isArray(field.value) ? field.value : [];
|
||
|
||
const add = () => {
|
||
const value = draft.trim();
|
||
if (!value) return;
|
||
if (items.includes(value)) {
|
||
setDraft('');
|
||
return;
|
||
}
|
||
field.onChange([...items, value]);
|
||
setDraft('');
|
||
};
|
||
|
||
const remove = (idx: number) => {
|
||
field.onChange(items.filter((_, i) => i !== idx));
|
||
};
|
||
|
||
const onKey = (e: KeyboardEvent<HTMLInputElement>) => {
|
||
if (e.key === 'Enter' || e.key === ',') {
|
||
e.preventDefault();
|
||
add();
|
||
} else if (e.key === 'Backspace' && draft === '' && items.length > 0) {
|
||
field.onChange(items.slice(0, -1));
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div>
|
||
<span className="block font-mono text-xs uppercase tracking-widest text-[color:var(--color-cyan)] mb-1.5">
|
||
{label}
|
||
</span>
|
||
{hint && (
|
||
<span className="block font-mono text-[10px] text-[color:var(--color-text-dim)] mb-1.5">
|
||
{hint}
|
||
</span>
|
||
)}
|
||
<div className="flex flex-wrap gap-2 mb-2">
|
||
{items.map((item, idx) => (
|
||
<span
|
||
key={`${item}-${idx}`}
|
||
className="chip group cursor-pointer"
|
||
onClick={() => remove(idx)}
|
||
title="Cliquer pour retirer"
|
||
>
|
||
{item}
|
||
<span className="ml-1 opacity-50 group-hover:opacity-100 group-hover:text-[color:var(--color-magenta)]">
|
||
×
|
||
</span>
|
||
</span>
|
||
))}
|
||
</div>
|
||
<div className="flex gap-2">
|
||
<input
|
||
type="text"
|
||
value={draft}
|
||
onChange={(e) => setDraft(e.target.value)}
|
||
onKeyDown={onKey}
|
||
placeholder={placeholder}
|
||
className="flex-1 bg-[color:var(--color-surface-2)] border border-[color:var(--color-border)] focus:border-[color:var(--color-cyan)] focus:outline-none p-2 rounded text-sm font-mono text-[color:var(--color-text)] transition-colors"
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={add}
|
||
className="px-3 py-1.5 font-mono text-xs uppercase tracking-widest border border-[color:var(--color-cyan)] text-[color:var(--color-cyan)] rounded hover:bg-[color:var(--color-cyan)] hover:text-[color:var(--color-bg)] transition-colors"
|
||
>
|
||
+ add
|
||
</button>
|
||
</div>
|
||
<FieldError name={name} />
|
||
</div>
|
||
);
|
||
}}
|
||
/>
|
||
);
|
||
}
|