79 lines
3.4 KiB
TypeScript
79 lines
3.4 KiB
TypeScript
import { useEffect, useRef, useState, type ReactNode } from "react";
|
|
import { Button } from "./design-system/primitives";
|
|
import { Field } from "./design-system/Field";
|
|
|
|
export type ItemMode = "edit" | "delete";
|
|
|
|
/** In-place editing and confirmation, shared by habit headings and task rows. */
|
|
export function InlineItemForm({ name, kind, mode, disabled, children, onSave, onDelete, onClose, submitLabel, formLabel }: {
|
|
name: string;
|
|
kind: "habit" | "task";
|
|
mode: ItemMode;
|
|
disabled?: boolean;
|
|
children?: ReactNode;
|
|
submitLabel?: string;
|
|
formLabel?: string;
|
|
onSave: (name: string) => Promise<void>;
|
|
onDelete: () => Promise<void>;
|
|
onClose: () => void;
|
|
}) {
|
|
const form = useRef<HTMLFormElement>(null);
|
|
const saving = useRef(false);
|
|
const [draft, setDraft] = useState(name);
|
|
const [busy, setBusy] = useState(false);
|
|
const [error, setError] = useState("");
|
|
useEffect(() => {
|
|
const previous = document.activeElement as HTMLElement | null;
|
|
form.current?.querySelector<HTMLElement>(mode === "edit" ? "input" : "button")?.focus();
|
|
return () => { if (previous?.isConnected) previous.focus(); };
|
|
}, [mode]);
|
|
|
|
return (
|
|
<form ref={form} className={`ds-inline-item-form ds-inline-item-form--${kind}`} aria-label={formLabel ?? `${mode === "edit" ? "Edit" : kind === "habit" ? "Archive" : "Delete"} ${kind} ${name}`}
|
|
onKeyDown={(event) => {
|
|
if (event.key === "Escape" && !saving.current) { event.preventDefault(); onClose(); }
|
|
}}
|
|
onSubmit={async (event) => {
|
|
event.preventDefault();
|
|
if (saving.current || disabled) return;
|
|
if (mode === "edit" && !draft.trim()) { setError("Enter a name."); return; }
|
|
saving.current = true;
|
|
setBusy(true);
|
|
setError("");
|
|
try {
|
|
if (mode === "delete") await onDelete();
|
|
else await onSave(draft.trim());
|
|
onClose();
|
|
} catch (error) {
|
|
setError(error instanceof Error ? error.message : "Could not save your change. Try again.");
|
|
} finally {
|
|
saving.current = false;
|
|
setBusy(false);
|
|
}
|
|
}}>
|
|
{mode === "edit" ? (
|
|
<fieldset className="ds-inline-item-fields" disabled={busy || disabled}>
|
|
<Field label={kind === "habit" ? "Habit name" : "Task name"}>
|
|
{(id) => <input id={id} value={draft} required maxLength={200} onChange={(event) => setDraft(event.target.value)} />}
|
|
</Field>
|
|
{children}
|
|
</fieldset>
|
|
) : (
|
|
<p className="ds-inline-delete-copy">
|
|
{kind === "habit" ? "Archive" : "Delete"} <strong>{name}</strong>? {kind === "habit"
|
|
? "This removes the habit from your dashboard starting today. Earlier history is kept. You can restore it from Archived habits."
|
|
: "This removes the task from today and future check-ins. Earlier history is kept."}
|
|
</p>
|
|
)}
|
|
{error && <p className="ds-form-feedback" role="alert">{error}</p>}
|
|
<div className="ds-inline-form-actions">
|
|
{mode === "delete" && <Button variant="text" disabled={busy} onClick={onClose}>Cancel</Button>}
|
|
<Button type="submit" disabled={busy || disabled}>
|
|
{busy ? "Saving…" : mode === "delete" ? `${kind === "habit" ? "Archive" : "Delete"} ${kind}` : submitLabel ?? "Save changes"}
|
|
</Button>
|
|
{mode === "edit" && <Button variant="text" disabled={busy} onClick={onClose}>Cancel</Button>}
|
|
</div>
|
|
</form>
|
|
);
|
|
}
|