diff --git a/src/components/skills/view/ProviderSkills.tsx b/src/components/skills/view/ProviderSkills.tsx index e264189a..f7077209 100644 --- a/src/components/skills/view/ProviderSkills.tsx +++ b/src/components/skills/view/ProviderSkills.tsx @@ -214,6 +214,7 @@ export default function ProviderSkills({ selectedProvider, currentProjects }: Pr const [queuedFiles, setQueuedFiles] = useState([]); const [submitError, setSubmitError] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); + const [justInstalled, setJustInstalled] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); const [showInstallPath, setShowInstallPath] = useState(false); @@ -230,6 +231,7 @@ export default function ProviderSkills({ selectedProvider, currentProjects }: Pr setSearchQuery(''); setIsAddDialogOpen(false); setShowInstallPath(false); + setJustInstalled(false); }, [selectedProvider]); useEffect(() => { @@ -357,6 +359,7 @@ export default function ProviderSkills({ selectedProvider, currentProjects }: Pr }))); await addSkills({ entries }); setQueuedFiles([]); + setJustInstalled(true); setIsAddDialogOpen(false); } catch (error) { setSubmitError(error instanceof Error ? error.message : 'Failed to import skills'); @@ -369,6 +372,7 @@ export default function ProviderSkills({ selectedProvider, currentProjects }: Pr if (open) { setSubmitError(null); setShowInstallPath(false); + setJustInstalled(false); setIsAddDialogOpen(true); return; } @@ -376,6 +380,7 @@ export default function ProviderSkills({ selectedProvider, currentProjects }: Pr setQueuedFiles([]); setSubmitError(null); setShowInstallPath(false); + setJustInstalled(false); setIsAddDialogOpen(false); }, []); @@ -592,6 +597,7 @@ export default function ProviderSkills({ selectedProvider, currentProjects }: Pr size="sm" className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground" aria-label="Close add skill dialog" + disabled={isSubmitting} onClick={() => handleAddDialogOpenChange(false)} > @@ -605,7 +611,7 @@ export default function ProviderSkills({ selectedProvider, currentProjects }: Pr
- {(submitError || loadError || saveStatus === 'success') ? ( + {(submitError || loadError || (justInstalled && saveStatus === 'success')) ? (
handleAddDialogOpenChange(false)} > Cancel @@ -651,7 +658,7 @@ export default function ProviderSkills({ selectedProvider, currentProjects }: Pr
)} - {saveStatus === 'success' && !isAddDialogOpen && ( + {justInstalled && saveStatus === 'success' && !isAddDialogOpen && (
Skills saved successfully. diff --git a/src/shared/view/ui/ActionMenu.tsx b/src/shared/view/ui/ActionMenu.tsx index 54d0afc8..bf83690d 100644 --- a/src/shared/view/ui/ActionMenu.tsx +++ b/src/shared/view/ui/ActionMenu.tsx @@ -47,6 +47,13 @@ export default function ActionMenu({ }: ActionMenuProps) { const [isOpen, setIsOpen] = React.useState(false); const rootRef = React.useRef(null); + const triggerRef = React.useRef(null); + const menuRef = React.useRef(null); + // Whether closing should move focus back to the trigger. Set for keyboard + // (Escape) and item selection, but left false for outside pointer clicks so + // focus is not stolen from wherever the user clicked. + const restoreFocusRef = React.useRef(false); + const wasOpenRef = React.useRef(false); const menuId = React.useId(); React.useEffect(() => { @@ -63,6 +70,7 @@ export default function ActionMenu({ const closeOnEscape = (event: KeyboardEvent) => { if (event.key === 'Escape') { + restoreFocusRef.current = true; setIsOpen(false); } }; @@ -75,11 +83,32 @@ export default function ActionMenu({ }; }, [isOpen]); + // Move focus into the menu on open and back to the trigger on a keyboard or + // selection close, so keyboard and screen-reader navigation match the menu role. + React.useEffect(() => { + if (isOpen) { + wasOpenRef.current = true; + const menu = menuRef.current; + const firstItem = menu?.querySelector('[role="menuitem"]:not([disabled])'); + (firstItem ?? menu)?.focus(); + return; + } + + if (wasOpenRef.current) { + wasOpenRef.current = false; + if (restoreFocusRef.current) { + triggerRef.current?.focus(); + } + restoreFocusRef.current = false; + } + }, [isOpen]); + const runItem = (item: ActionMenuItem) => { if (item.disabled || item.loading) { return; } + restoreFocusRef.current = true; setIsOpen(false); item.onSelect(); }; @@ -87,6 +116,7 @@ export default function ActionMenu({ return (