feat(tickets): fenêtre dédiée de gestion des tickets — liens via popup + assistant IA flottant (#17)
Lot C du sprint UI rework : fenêtre dédiée de gestion des tickets, appuyée sur la primitive FloatingWindow (#16) et la popup TicketPicker (#18). - TicketDetail : gestion des liens de ticket via la popup TicketPicker, assistant IA en fenêtre flottante - FloatingWindow : ajustements pour l'usage fenêtre dédiée Tests : tsc --noEmit clean, suite complète 530/530, suites tickets + FloatingWindow 50/50. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -34,6 +34,13 @@ const SIZE_CLASS: Record<FloatingWindowSize, string> = {
|
||||
const FOCUSABLE =
|
||||
'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])';
|
||||
|
||||
/**
|
||||
* Stack of currently-mounted windows (most-recent last). Only the topmost window
|
||||
* reacts to Escape and traps Tab, so a window opened over another (e.g. a ticket
|
||||
* detail over the tickets list) doesn't close/steal from the one beneath it.
|
||||
*/
|
||||
const windowStack: object[] = [];
|
||||
|
||||
export interface FloatingWindowProps {
|
||||
/** Whether the window is shown. Rendered as `null` when closed. */
|
||||
open: boolean;
|
||||
@ -76,11 +83,17 @@ function FloatingWindowBody({
|
||||
useEffect(() => {
|
||||
const previouslyFocused = document.activeElement as HTMLElement | null;
|
||||
const node = dialogRef.current;
|
||||
// Register on the window stack so only the topmost handles keys.
|
||||
const token = {};
|
||||
windowStack.push(token);
|
||||
const isTopmost = () => windowStack[windowStack.length - 1] === token;
|
||||
// Move focus into the window (first focusable, else the dialog itself).
|
||||
const first = node?.querySelector<HTMLElement>(FOCUSABLE);
|
||||
(first ?? node)?.focus();
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
// Ignore keys while a later window is stacked on top of this one.
|
||||
if (!isTopmost()) return;
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
onClose();
|
||||
@ -111,6 +124,8 @@ function FloatingWindowBody({
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
const i = windowStack.indexOf(token);
|
||||
if (i >= 0) windowStack.splice(i, 1);
|
||||
previouslyFocused?.focus?.();
|
||||
};
|
||||
}, [onClose]);
|
||||
|
||||
Reference in New Issue
Block a user