Skip to content

Commit 85feda0

Browse files
committed
fix(webapp): make every side menu item keyboard-focusable in DOM order
SimpleTooltip removed its trigger from the tab order (tabIndex=-1), so every side menu item wrapped in one (the nav links and the org/project/environment/account selectors) was skipped when tabbing. Add an opt-in tabbable prop to SimpleTooltip and set it on those triggers, give each the focus-custom ring, and turn the collapsible section headers into real buttons so they are keyboard-operable too. Tab order now follows the DOM top to bottom in both the expanded and collapsed states.
1 parent aec6288 commit 85feda0

6 files changed

Lines changed: 28 additions & 8 deletions

File tree

apps/webapp/app/components/navigation/EnvironmentSelector.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export function EnvironmentSelector({
7070
button={
7171
<PopoverTrigger
7272
className={cn(
73-
"group flex h-8 items-center rounded pl-[0.4375rem] hover:bg-charcoal-750",
73+
"group flex h-8 items-center rounded pl-[0.4375rem] hover:bg-charcoal-750 focus-custom",
7474
isCollapsed ? "justify-center pr-0.5" : "justify-between pr-1",
7575
className
7676
)}
@@ -113,6 +113,7 @@ export function EnvironmentSelector({
113113
delayDuration={isCollapsed ? 0 : 500}
114114
buttonClassName="!h-8"
115115
asChild
116+
tabbable
116117
disableHoverableContent
117118
/>
118119
<PopoverContent

apps/webapp/app/components/navigation/HelpAndFeedbackPopover.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ export function HelpAndFeedback({
108108
delayDuration={isCollapsed ? 0 : 500}
109109
buttonClassName="!h-8 w-full"
110110
asChild
111+
tabbable
111112
disableHoverableContent
112113
/>
113114
<PopoverContent

apps/webapp/app/components/navigation/SideMenu.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1156,7 +1156,7 @@ function OrgSelector({
11561156
button={
11571157
<PopoverTrigger
11581158
className={cn(
1159-
"group flex h-8 items-center rounded pl-[0.4375rem] hover:bg-charcoal-750",
1159+
"group flex h-8 items-center rounded pl-[0.4375rem] hover:bg-charcoal-750 focus-custom",
11601160
isCollapsed ? "justify-center pr-0.5" : "w-full justify-between pr-1"
11611161
)}
11621162
>
@@ -1192,6 +1192,7 @@ function OrgSelector({
11921192
hidden={!isCollapsed}
11931193
buttonClassName="!h-8"
11941194
asChild
1195+
tabbable
11951196
disableHoverableContent
11961197
/>
11971198
<PopoverContent
@@ -1333,6 +1334,7 @@ function AccountMenu({ isAdmin, isImpersonating }: { isAdmin: boolean; isImperso
13331334
side="bottom"
13341335
sideOffset={8}
13351336
asChild
1337+
tabbable
13361338
disableHoverableContent
13371339
/>
13381340
<PopoverContent
@@ -1443,7 +1445,7 @@ function ProjectSelector({
14431445
button={
14441446
<PopoverTrigger
14451447
className={cn(
1446-
"group flex h-8 items-center rounded border pl-[0.4375rem] transition-[border-color] duration-150 hover:bg-charcoal-750",
1448+
"group flex h-8 items-center rounded border pl-[0.4375rem] transition-[border-color] duration-150 hover:bg-charcoal-750 focus-custom",
14471449
isCollapsed
14481450
? "justify-center border-transparent pr-0.5"
14491451
: "justify-between border-charcoal-700 pr-1",
@@ -1482,6 +1484,7 @@ function ProjectSelector({
14821484
hidden={!isCollapsed}
14831485
buttonClassName="!h-8"
14841486
asChild
1487+
tabbable
14851488
disableHoverableContent
14861489
/>
14871490
<PopoverContent

apps/webapp/app/components/navigation/SideMenuItem.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export function SideMenuItem({
6363
target={target}
6464
data-action={dataAction}
6565
className={cn(
66-
"group/menulink flex h-8 items-center gap-2 overflow-hidden rounded pl-[0.4375rem] pr-2",
66+
"group/menulink flex h-8 items-center gap-2 overflow-hidden rounded pl-[0.4375rem] pr-2 focus-custom",
6767
isIndented ? "min-w-0 flex-1" : "w-full",
6868
isActive
6969
? "bg-tertiary text-text-bright"
@@ -140,6 +140,7 @@ export function SideMenuItem({
140140
buttonClassName="!h-8 block w-full"
141141
hidden={!isCollapsed}
142142
asChild
143+
tabbable
143144
disableHoverableContent
144145
/>
145146
{!isCollapsed && (
@@ -168,6 +169,7 @@ export function SideMenuItem({
168169
buttonClassName="!h-8 block w-full"
169170
hidden={!isCollapsed}
170171
asChild
172+
tabbable
171173
disableHoverableContent
172174
/>
173175
);

apps/webapp/app/components/navigation/SideMenuSection.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,15 @@ export function SideMenuSection({
4444
real time while dragging. The hover background and text color snap (no transition), to
4545
match the nav items.
4646
*/}
47-
<div
48-
className="group/section flex cursor-pointer items-center justify-between overflow-hidden rounded-sm py-1 pl-1.5 pr-1 hover:bg-charcoal-750"
47+
<button
48+
type="button"
49+
// A real button so it's keyboard-operable (Enter/Space toggle natively) and shows the
50+
// focus-custom ring. Removed from the tab order when the side menu is collapsed, since
51+
// the header is hidden (the divider takes its place) and can't be toggled then.
52+
className="group/section flex w-full cursor-pointer items-center justify-between overflow-hidden rounded-sm py-1 pl-1.5 pr-1 hover:bg-charcoal-750 focus-custom"
4953
onClick={isSideMenuCollapsed ? undefined : handleToggle}
54+
tabIndex={isSideMenuCollapsed ? -1 : undefined}
55+
aria-expanded={!isCollapsed}
5056
style={{
5157
opacity: "var(--sm-label-opacity, 1)",
5258
cursor: isSideMenuCollapsed ? "default" : "pointer",
@@ -63,7 +69,7 @@ export function SideMenuSection({
6369
</motion.div>
6470
</div>
6571
{headerAction && <div className="flex items-center">{headerAction}</div>}
66-
</div>
72+
</button>
6773
{/*
6874
Divider - absolutely positioned, fades in as the header fades out (driven by the
6975
`--sm-collapse` progress variable, 0 → 1). Only shown while this section is expanded.

apps/webapp/app/components/primitives/Tooltip.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ function SimpleTooltip({
6363
buttonClassName,
6464
buttonStyle,
6565
asChild = false,
66+
tabbable = false,
6667
sideOffset,
6768
open,
6869
onOpenChange,
@@ -78,6 +79,12 @@ function SimpleTooltip({
7879
buttonClassName?: string;
7980
buttonStyle?: React.CSSProperties;
8081
asChild?: boolean;
82+
/**
83+
* By default the trigger is removed from the tab order (`tabIndex={-1}`) so decorative tooltips
84+
* don't add tab stops. Set this when the trigger wraps a genuinely interactive element (a link
85+
* or button) that should stay keyboard-focusable.
86+
*/
87+
tabbable?: boolean;
8188
sideOffset?: number;
8289
open?: boolean;
8390
onOpenChange?: (open: boolean) => void;
@@ -88,7 +95,7 @@ function SimpleTooltip({
8895
<Tooltip open={open} onOpenChange={onOpenChange} delayDuration={delayDuration}>
8996
<TooltipTrigger
9097
type={asChild ? undefined : "button"}
91-
tabIndex={-1}
98+
tabIndex={tabbable ? undefined : -1}
9299
className={cn(!asChild && "h-fit", buttonClassName)}
93100
style={buttonStyle}
94101
asChild={asChild}

0 commit comments

Comments
 (0)