diff --git a/apps/web/src/app/(app)/resources/ResourcesClient.tsx b/apps/web/src/app/(app)/resources/ResourcesClient.tsx index 12742f1..2cd73ae 100644 --- a/apps/web/src/app/(app)/resources/ResourcesClient.tsx +++ b/apps/web/src/app/(app)/resources/ResourcesClient.tsx @@ -35,6 +35,7 @@ import { useViewPrefs } from "~/hooks/useViewPrefs.js"; import { useRowOrder } from "~/hooks/useRowOrder.js"; import { useAnchoredOverlay } from "~/hooks/useAnchoredOverlay.js"; import { DraggableTableRow } from "~/components/ui/DraggableTableRow.js"; +import { SuccessToast } from "~/components/ui/SuccessToast.js"; type ModalState = | { type: "closed" } @@ -158,6 +159,7 @@ export function ResourcesClient() { const [departedFilter, setDepartedFilter] = useState(DEFAULT_BOOLEAN_FILTER); const [modal, setModal] = useState({ type: "closed" }); const [confirm, setConfirm] = useState({ type: "closed" }); + const [successToast, setSuccessToast] = useState(null); const selection = useSelection(); const utils = trpc.useUtils(); @@ -1427,7 +1429,13 @@ export function ResourcesClient() { ]} /> - {modal.type === "create" && } + {modal.type === "create" && ( + setSuccessToast(`Resource "${name}" created successfully.`)} + /> + )} {modal.type === "edit" && ( )} @@ -1461,6 +1469,12 @@ export function ResourcesClient() { onCancel={() => setConfirm({ type: "closed" })} /> )} + + setSuccessToast(null)} + /> ); } diff --git a/apps/web/src/components/resources/ResourceModal.tsx b/apps/web/src/components/resources/ResourceModal.tsx index 91936d9..48c2fbd 100644 --- a/apps/web/src/components/resources/ResourceModal.tsx +++ b/apps/web/src/components/resources/ResourceModal.tsx @@ -161,6 +161,7 @@ interface ResourceModalProps { mode: "create" | "edit"; resource?: Resource; onClose: () => void; + onSuccess?: (displayName: string) => void; } const INPUT_CLASS = @@ -189,7 +190,7 @@ function Spinner() { ); } -export function ResourceModal({ mode, resource, onClose }: ResourceModalProps) { +export function ResourceModal({ mode, resource, onClose, onSuccess }: ResourceModalProps) { const [form, setForm] = useState(() => resource ? resourceToFormState(resource) : defaultFormState(), ); @@ -324,7 +325,12 @@ export function ResourceModal({ mode, resource, onClose }: ResourceModalProps) { try { if (mode === "create") { - await createMutation.mutateAsync(payload); + const created = await createMutation.mutateAsync(payload); + void utils.resource.directory.invalidate(); + void utils.resource.listStaff.invalidate(); + onSuccess?.(created.displayName); + onClose(); + return; } else if (resource) { await updateMutation.mutateAsync({ id: resource.id, data: payload }); } diff --git a/apps/web/src/hooks/useFocusTrap.ts b/apps/web/src/hooks/useFocusTrap.ts index 05f5242..9c44e74 100644 --- a/apps/web/src/hooks/useFocusTrap.ts +++ b/apps/web/src/hooks/useFocusTrap.ts @@ -8,16 +8,22 @@ export function useFocusTrap(ref: React.RefObject, isOpen: b if (!isOpen || !ref.current) return; const el = ref.current; - const focusable = Array.from(el.querySelectorAll(FOCUSABLE_SELECTOR)); - const first = focusable[0]; - const last = focusable[focusable.length - 1]; - - // Focus first element when modal opens - first?.focus(); + // Defer initial focus to the next animation frame so the browser layout is + // complete before we attempt to focus — prevents intermittent failures when + // the modal DOM has just been mounted. + const rafId = requestAnimationFrame(() => { + const focusable = Array.from(el.querySelectorAll(FOCUSABLE_SELECTOR)); + focusable[0]?.focus(); + }); function handleKeyDown(e: KeyboardEvent) { if (e.key !== "Tab") return; + // Re-query on every keydown so the list is always current — form state can + // toggle disabled attributes and add/remove sections during a session. + const focusable = Array.from(el.querySelectorAll(FOCUSABLE_SELECTOR)); if (focusable.length === 0) { e.preventDefault(); return; } + const first = focusable[0]; + const last = focusable[focusable.length - 1]; if (e.shiftKey) { if (document.activeElement === first) { e.preventDefault(); last?.focus(); } } else { @@ -26,6 +32,9 @@ export function useFocusTrap(ref: React.RefObject, isOpen: b } el.addEventListener("keydown", handleKeyDown); - return () => el.removeEventListener("keydown", handleKeyDown); + return () => { + cancelAnimationFrame(rafId); + el.removeEventListener("keydown", handleKeyDown); + }; }, [isOpen, ref]); }