fix(ux): resolve tickets #55 #56 — resource modal stability and success feedback

#55: Add SuccessToast after new resource is created. ResourceModal gains an
optional onSuccess(displayName) prop; ResourcesClient wires it to a toast
that auto-dismisses after 2.5 s.

#56: Fix useFocusTrap stale-closure bug. Focusable elements are now queried
dynamically inside handleKeyDown (not captured once at mount), so Tab
navigation stays correct as the form re-renders. Initial focus is deferred
via requestAnimationFrame so the browser layout is stable before focus() fires.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-04-03 15:43:12 +02:00
parent 0d0707264d
commit 65db330a4d
3 changed files with 39 additions and 10 deletions
+16 -7
View File
@@ -8,16 +8,22 @@ export function useFocusTrap(ref: React.RefObject<HTMLElement | null>, isOpen: b
if (!isOpen || !ref.current) return;
const el = ref.current;
const focusable = Array.from(el.querySelectorAll<HTMLElement>(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<HTMLElement>(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<HTMLElement>(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<HTMLElement | null>, isOpen: b
}
el.addEventListener("keydown", handleKeyDown);
return () => el.removeEventListener("keydown", handleKeyDown);
return () => {
cancelAnimationFrame(rafId);
el.removeEventListener("keydown", handleKeyDown);
};
}, [isOpen, ref]);
}