From 7f8cf30ca479474f8c6e0a85f6e0a1169bcca365 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Fri, 4 Sep 2026 22:39:59 -0400 Subject: [PATCH] fix(web): make sidebar project actions reachable by keyboard and screen reader (#5521) Co-authored-by: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> --- apps/web/src/components/Sidebar.tsx | 42 +++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 2e962810d62195306c5a306f4c3f124d2406ead5..77b7b0abf0a68b7f3439613724c8b2fc38e737fd 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -2223,10 +2223,18 @@ export default function Sidebar() { }, [isMobile, router, setOpenMobile], ); + // Safari can send a click after Ctrl+click opens settings. Ignore that one + // selection, then clear the guard when the picker opens again. + const suppressNextScopeChangeRef = useRef(false); + const highlightedProjectScopeKeyRef = useRef(null); const handleProjectSettings = useCallback( - (event: ReactMouseEvent, projectGroup: SidebarProjectSnapshot) => { + ( + event: ReactMouseEvent | ReactKeyboardEvent, + projectGroup: SidebarProjectSnapshot, + ) => { event.preventDefault(); event.stopPropagation(); + suppressNextScopeChangeRef.current = true; dispatchProjectScopeMenu({ type: "project-settings-opened" }); openProjectSettings(projectGroup); }, @@ -3724,10 +3732,18 @@ export default function Sidebar() { isItemEqualToValue={(a, b) => a.value === b.value} open={projectScopeMenuState.open} onOpenChange={(open) => { + if (open) suppressNextScopeChangeRef.current = false; dispatchProjectScopeMenu({ type: "open-changed", open }); }} + onItemHighlighted={(item) => { + highlightedProjectScopeKeyRef.current = item?.value ?? null; + }} value={selectedProjectScopeItem} onValueChange={(item) => { + if (suppressNextScopeChangeRef.current) { + suppressNextScopeChangeRef.current = false; + return; + } if (!item) return; setProjectScopeKey(item.value === "all" ? null : item.value); }} @@ -3778,6 +3794,24 @@ export default function Sidebar() { size="sm" unstyled value={projectScopeMenuState.query} + onKeyDown={(event) => { + if ( + event.defaultPrevented || + event.nativeEvent.isComposing || + event.ctrlKey || + event.altKey || + event.metaKey || + (event.key !== "ContextMenu" && + !(event.shiftKey && event.key === "F10")) + ) { + return; + } + // Combobox items use virtual focus: keyboard events + // stay on this input, not on the highlighted option. + const scopeKey = highlightedProjectScopeKeyRef.current; + const project = scopeKey ? projectGroupByScopeKey.get(scopeKey) : null; + if (project) handleProjectSettings(event, project); + }} onChange={(event) => dispatchProjectScopeMenu({ type: "query-changed", @@ -3798,6 +3832,9 @@ export default function Sidebar() { value={item} className="h-8 min-h-8 py-0 font-medium" contentClassName="flex min-w-0 items-center gap-2" + onContextMenu={(event) => { + if (project) handleProjectSettings(event, project); + }} > {project ? (