fix(tickets): corrige la perte de focus dans l'édition d'un ticket (#17)
Le focus-trap de FloatingWindow avait un useEffect dépendant de `[onClose]`. `onClose` (ex. TicketDetail.requestClose) est une closure recréée à chaque render : chaque frappe ré-armait l'effet et renvoyait le focus au premier élément focusable, ne laissant saisir qu'une lettre à la fois dans le formulaire d'édition. Correctif : conserver le dernier `onClose` dans un `onCloseRef` et passer l'effet focus-trap en mount-only (`[]`). Le handler Escape lit la valeur courante via la ref. Ajout d'un test de régression prouvé rouge-sans / vert-avec le fix. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -80,6 +80,15 @@ function FloatingWindowBody({
|
||||
}: Omit<FloatingWindowProps, "open">) {
|
||||
const dialogRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Keep the latest `onClose` in a ref so the mount-only effect below never
|
||||
// re-runs when the caller passes an unstable `onClose` closure (a new
|
||||
// identity on each parent render). Re-running the effect would move focus
|
||||
// back to the first focusable element on every keystroke, so a controlled
|
||||
// input inside the window could only accept one character before losing
|
||||
// focus. The Escape handler reads the current value via this ref.
|
||||
const onCloseRef = useRef(onClose);
|
||||
onCloseRef.current = onClose;
|
||||
|
||||
useEffect(() => {
|
||||
const previouslyFocused = document.activeElement as HTMLElement | null;
|
||||
const node = dialogRef.current;
|
||||
@ -96,7 +105,7 @@ function FloatingWindowBody({
|
||||
if (!isTopmost()) return;
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
onClose();
|
||||
onCloseRef.current();
|
||||
return;
|
||||
}
|
||||
if (e.key !== "Tab" || !node) return;
|
||||
@ -128,7 +137,10 @@ function FloatingWindowBody({
|
||||
if (i >= 0) windowStack.splice(i, 1);
|
||||
previouslyFocused?.focus?.();
|
||||
};
|
||||
}, [onClose]);
|
||||
// Mount-only: focus save/restore and listener wiring must not re-run when
|
||||
// `onClose` changes identity (see `onCloseRef` above).
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user