From 8632cee40a7af4866180e2f83eed301589ef5713 Mon Sep 17 00:00:00 2001 From: anti Date: Wed, 22 Apr 2026 17:09:45 -0400 Subject: [PATCH] feat(web): retrofit drawers + CreateTopologyWizard with ESC/focus-trap ArtifactDrawer, SessionDrawer, CreateTopologyWizard all now: - close on ESC - trap Tab/Shift+Tab focus within the panel - lock body scroll while open - restore prior focus on unmount Uses the new useEscapeKey + useFocusTrap hooks. No visual changes; the bespoke CSS shells (ctw-*, inline drawer styling) are preserved. --- decnet_web/src/components/ArtifactDrawer.tsx | 16 +++++++++++++++- decnet_web/src/components/SessionDrawer.tsx | 14 ++++++++++++++ .../TopologyList/CreateTopologyWizard.tsx | 17 +++++++++++++++-- 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/decnet_web/src/components/ArtifactDrawer.tsx b/decnet_web/src/components/ArtifactDrawer.tsx index 491ec9c7..a0be33b0 100644 --- a/decnet_web/src/components/ArtifactDrawer.tsx +++ b/decnet_web/src/components/ArtifactDrawer.tsx @@ -1,6 +1,8 @@ -import React, { useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { X, Download, AlertTriangle } from 'lucide-react'; import api from '../utils/api'; +import { useEscapeKey } from '../hooks/useEscapeKey'; +import { useFocusTrap } from '../hooks/useFocusTrap'; interface ArtifactDrawerProps { decky: string; @@ -32,6 +34,15 @@ const Row: React.FC<{ label: string; value: React.ReactNode }> = ({ label, value ); const ArtifactDrawer: React.FC = ({ decky, storedAs, fields, onClose }) => { + const panelRef = useRef(null); + useEscapeKey(onClose, true); + useFocusTrap(panelRef, true); + useEffect(() => { + const prev = document.body.style.overflow; + document.body.style.overflow = 'hidden'; + return () => { document.body.style.overflow = prev; }; + }, []); + const [downloading, setDownloading] = useState(false); const [error, setError] = useState(null); const meta = decodeMeta(fields); @@ -80,6 +91,9 @@ const ArtifactDrawer: React.FC = ({ decky, storedAs, fields }} >
e.stopPropagation()} style={{ width: 'min(620px, 100%)', height: '100%', diff --git a/decnet_web/src/components/SessionDrawer.tsx b/decnet_web/src/components/SessionDrawer.tsx index eeee3ead..2a3e8fd8 100644 --- a/decnet_web/src/components/SessionDrawer.tsx +++ b/decnet_web/src/components/SessionDrawer.tsx @@ -1,6 +1,8 @@ import React, { useEffect, useRef, useState } from 'react'; import { X, AlertTriangle } from 'lucide-react'; import api from '../utils/api'; +import { useEscapeKey } from '../hooks/useEscapeKey'; +import { useFocusTrap } from '../hooks/useFocusTrap'; // @ts-expect-error -- ships without type defs; 3.x CJS build is used directly import * as AsciinemaPlayer from 'asciinema-player'; import 'asciinema-player/dist/bundle/asciinema-player.css'; @@ -46,6 +48,15 @@ function buildCastBlob(header: Record, events: [number, string, str } const SessionDrawer: React.FC = ({ decky, sid, fields, onClose }) => { + const panelRef = useRef(null); + useEscapeKey(onClose, true); + useFocusTrap(panelRef, true); + useEffect(() => { + const prev = document.body.style.overflow; + document.body.style.overflow = 'hidden'; + return () => { document.body.style.overflow = prev; }; + }, []); + const [header, setHeader] = useState | null>(null); const [events, setEvents] = useState<[number, string, string][]>([]); const [loading, setLoading] = useState(true); @@ -145,6 +156,9 @@ const SessionDrawer: React.FC = ({ decky, sid, fields, onClo }} >
e.stopPropagation()} style={{ width: 'min(920px, 100%)', height: '100%', diff --git a/decnet_web/src/components/TopologyList/CreateTopologyWizard.tsx b/decnet_web/src/components/TopologyList/CreateTopologyWizard.tsx index ed33acf3..0116140f 100644 --- a/decnet_web/src/components/TopologyList/CreateTopologyWizard.tsx +++ b/decnet_web/src/components/TopologyList/CreateTopologyWizard.tsx @@ -1,6 +1,8 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { X, Server, Cpu, FileText, Sparkles, Check } from 'lucide-react'; import api from '../../utils/api'; +import { useEscapeKey } from '../../hooks/useEscapeKey'; +import { useFocusTrap } from '../../hooks/useFocusTrap'; import './CreateTopologyWizard.css'; /* Shape of GET /swarm/hosts rows (mirrors SwarmHostView). */ @@ -36,6 +38,17 @@ interface Props { const LOCAL_CARD_ID = '__local__'; const CreateTopologyWizard: React.FC = ({ open, onClose, onCreated }) => { + const panelRef = useRef(null); + useEscapeKey(onClose, open); + useFocusTrap(panelRef, open); + + useEffect(() => { + if (!open) return; + const prev = document.body.style.overflow; + document.body.style.overflow = 'hidden'; + return () => { document.body.style.overflow = prev; }; + }, [open]); + const [step, setStep] = useState<0 | 1>(0); const [targetId, setTargetId] = useState(null); // LOCAL_CARD_ID or host uuid const [kind, setKind] = useState(null); @@ -228,7 +241,7 @@ const CreateTopologyWizard: React.FC = ({ open, onClose, onCreated }) => return (
-
e.stopPropagation()}> +
e.stopPropagation()}>

NEW TOPOLOGY