From f25ce8db736a1c947472b8aace17668737c524ba Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Wed, 27 May 2026 09:14:29 +0100 Subject: [PATCH] fix(a11y): add accessibility and UX polish to DirectoryPicker - Add role="dialog", aria-modal, aria-label to the modal panel - Add aria-label to close button, home button - Add aria-hidden to decorative icons (chevrons, backdrop) - Add role="status" to loading spinner with sr-only label - Add role="alert" to error state - Add aria-current="location" to active breadcrumb segment - Wrap breadcrumb in nav landmark with aria-label - Add Escape key handler to dismiss the modal - Auto-focus the modal panel on open - Add focus-visible ring styles to all interactive elements (matches existing focus-visible:ring-2 ring-accent/40 pattern) - Increase breadcrumb button padding (px-1.5 py-1) for better touch targets - Increase directory entry padding (py-2.5) for touch comfort - Add active:bg-hover/70 pressed state on directory entries - Add active:bg-accent/80 pressed state on select button --- .../src/components/DirectoryPicker.tsx | 60 ++++++++++++++----- 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/gitnexus-web/src/components/DirectoryPicker.tsx b/gitnexus-web/src/components/DirectoryPicker.tsx index dfb46dc6e..5f144faa6 100644 --- a/gitnexus-web/src/components/DirectoryPicker.tsx +++ b/gitnexus-web/src/components/DirectoryPicker.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; import { Folder, ChevronRight, Home, X, Loader2, AlertCircle, Check } from '@/lib/lucide-icons'; import { listDirectories, type DirEntry } from '../services/backend-client'; import { useTranslation } from 'react-i18next'; @@ -17,6 +17,7 @@ export const DirectoryPicker = ({ open, onClose, onSelect, initialDir }: Directo const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [isVisible, setIsVisible] = useState(false); + const panelRef = useRef(null); const fetchEntries = useCallback(async (dir: string) => { setLoading(true); @@ -41,6 +42,24 @@ export const DirectoryPicker = ({ open, onClose, onSelect, initialDir }: Directo } }, [open, initialDir, fetchEntries]); + useEffect(() => { + if (!open) return; + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + e.stopPropagation(); + onClose(); + } + }; + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [open, onClose]); + + useEffect(() => { + if (open && panelRef.current) { + panelRef.current.focus(); + } + }, [open]); + if (!open) return null; const segments = currentDir.split('/').filter(Boolean); @@ -59,11 +78,17 @@ export const DirectoryPicker = ({ open, onClose, onSelect, initialDir }: Directo