Files
siftlode/frontend/src/components/UserPicker.tsx
T

56 lines
1.7 KiB
TypeScript

import Combobox from "./ui/Combobox";
// Pick one registered user by name or email: the download quota's "any user" picker and the share
// dialog's recipient. Both used to hand-roll an input over an absolute list that only a mouse could
// reach (no arrow keys, no listbox for a screen reader, closed by a blur timer); this is the shared
// Combobox in single-select, which brings all of that. A pick is an ACTION here, not a value to show,
// so nothing stays selected: the field clears for the next one.
export interface PickableUser {
id: number;
email: string;
display_name?: string | null;
}
export default function UserPicker<U extends PickableUser>({
users,
onPick,
label,
placeholder,
loading,
}: {
users: readonly U[];
onPick: (u: U) => void;
label: string;
placeholder: string;
loading?: boolean;
}) {
const byId = new Map(users.map((u) => [String(u.id), u]));
return (
<Combobox
label={label}
placeholder={placeholder}
multiple={false}
loading={loading}
selected={[]}
// The label carries both, so the client filter matches a name OR an email.
options={users.map((u) => ({
value: String(u.id),
label: u.display_name ? `${u.display_name} · ${u.email}` : u.email,
}))}
onChange={(next) => {
const u = next[0] != null ? byId.get(next[0]) : undefined;
if (u) onPick(u);
}}
renderOption={(o) => {
const u = byId.get(o.value);
return (
<span className="flex min-w-0 flex-col">
<span className="truncate">{u?.display_name || u?.email}</span>
{u?.display_name && <span className="truncate text-xs text-muted">{u.email}</span>}
</span>
);
}}
/>
);
}