authorgravatar for andrew@johnson5.netAndrew Johnson <andrew@johnson5.net> 2026-09-04 22:39:59-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-04 19:39:59-07:00
log7f8cf30ca479474f8c6e0a85f6e0a1169bcca365
treeb49cfac94f78659edd12bfd01b9124457b5de724
parent1246146f59a5a0f7a6f2da46bdd91f79b231d6a6
signature Signed by PGP key B5690EEEBB952194

fix(web): make sidebar project actions reachable by keyboard and screen reader (#5521)

Co-authored-by: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com>

1 files changed, 40 insertions(+), 2 deletions(-)

apps/web/src/components/Sidebar.tsx+40-2
......@@ -2223,10 +2223,18 @@ export default function Sidebar() {
22232223 },
22242224 [isMobile, router, setOpenMobile],
22252225 );
2226 // Safari can send a click after Ctrl+click opens settings. Ignore that one
2227 // selection, then clear the guard when the picker opens again.
2228 const suppressNextScopeChangeRef = useRef(false);
2229 const highlightedProjectScopeKeyRef = useRef<string | null>(null);
22262230 const handleProjectSettings = useCallback(
2227 (event: ReactMouseEvent<HTMLButtonElement>, projectGroup: SidebarProjectSnapshot) => {
2231 (
2232 event: ReactMouseEvent<HTMLElement> | ReactKeyboardEvent<HTMLInputElement>,
2233 projectGroup: SidebarProjectSnapshot,
2234 ) => {
22282235 event.preventDefault();
22292236 event.stopPropagation();
2237 suppressNextScopeChangeRef.current = true;
22302238 dispatchProjectScopeMenu({ type: "project-settings-opened" });
22312239 openProjectSettings(projectGroup);
22322240 },
......@@ -3724,10 +3732,18 @@ export default function Sidebar() {
37243732 isItemEqualToValue={(a, b) => a.value === b.value}
37253733 open={projectScopeMenuState.open}
37263734 onOpenChange={(open) => {
3735 if (open) suppressNextScopeChangeRef.current = false;
37273736 dispatchProjectScopeMenu({ type: "open-changed", open });
37283737 }}
3738 onItemHighlighted={(item) => {
3739 highlightedProjectScopeKeyRef.current = item?.value ?? null;
3740 }}
37293741 value={selectedProjectScopeItem}
37303742 onValueChange={(item) => {
3743 if (suppressNextScopeChangeRef.current) {
3744 suppressNextScopeChangeRef.current = false;
3745 return;
3746 }
37313747 if (!item) return;
37323748 setProjectScopeKey(item.value === "all" ? null : item.value);
37333749 }}
......@@ -3778,6 +3794,24 @@ export default function Sidebar() {
37783794 size="sm"
37793795 unstyled
37803796 value={projectScopeMenuState.query}
3797 onKeyDown={(event) => {
3798 if (
3799 event.defaultPrevented ||
3800 event.nativeEvent.isComposing ||
3801 event.ctrlKey ||
3802 event.altKey ||
3803 event.metaKey ||
3804 (event.key !== "ContextMenu" &&
3805 !(event.shiftKey && event.key === "F10"))
3806 ) {
3807 return;
3808 }
3809 // Combobox items use virtual focus: keyboard events
3810 // stay on this input, not on the highlighted option.
3811 const scopeKey = highlightedProjectScopeKeyRef.current;
3812 const project = scopeKey ? projectGroupByScopeKey.get(scopeKey) : null;
3813 if (project) handleProjectSettings(event, project);
3814 }}
37813815 onChange={(event) =>
37823816 dispatchProjectScopeMenu({
37833817 type: "query-changed",
......@@ -3798,6 +3832,9 @@ export default function Sidebar() {
37983832 value={item}
37993833 className="h-8 min-h-8 py-0 font-medium"
38003834 contentClassName="flex min-w-0 items-center gap-2"
3835 onContextMenu={(event) => {
3836 if (project) handleProjectSettings(event, project);
3837 }}
38013838 >
38023839 {project ? (
38033840 <ProjectFavicon
......@@ -3816,7 +3853,8 @@ export default function Sidebar() {
38163853 <Button
38173854 size="icon-xs"
38183855 variant="ghost-muted"
3819 aria-label={`Project settings for ${project.displayName}`}
3856 tabIndex={-1}
3857 aria-hidden="true"
38203858 title={`Project settings for ${project.displayName}`}
38213859 className="ml-auto size-6 [--control-icon-color:currentColor] text-icon-muted focus-visible:bg-accent focus-visible:text-foreground"
38223860 onPointerDown={(event) => event.stopPropagation()}