Fix sidebar user menu accessibility and full-width behavior

This commit is contained in:
Mike Cao committed 2026-08-02 12:43:07 -07:00
1 parent fab78f1610
commit d336eba27d
4 files changed
+75 -21

No files matched your search

+1 -1
View File
@@ -144,7 +144,7 @@ export function SideNav(props: any) {
</Column>
)}
</Column>
<Row paddingTop="2">
<Row paddingTop="2" width="100%" style={{ flexShrink: 0 }}>
<UserButton showText={!isCollapsed} />
</Row>
</Column>
+3 -3
View File
@@ -1,11 +1,11 @@
import {
Button,
DialogTrigger,
Icon,
Menu,
MenuItem,
MenuSection,
MenuSeparator,
MenuTrigger,
Popover,
Row,
Text,
@@ -53,7 +53,7 @@ export function TeamsButton() {
};
return (
<MenuTrigger>
<DialogTrigger>
<Button variant="quiet">
<Row
alignItems="center"
@@ -100,6 +100,6 @@ export function TeamsButton() {
</MenuSection>
</Menu>
</Popover>
</MenuTrigger>
</DialogTrigger>
);
}
+46
View File
@@ -0,0 +1,46 @@
import { expect, test, vi } from 'vitest';
import { render, screen } from '@/test/render';
import { UserButton } from './UserButton';
vi.mock('next/navigation', () => ({
useRouter: () => ({ push: vi.fn() }),
}));
vi.mock('@/components/hooks', () => ({
useConfig: () => ({ cloudMode: false }),
useLocale: () => ({ locale: 'en-US', saveLocale: vi.fn() }),
useLoginQuery: () => ({ user: { username: 'admin', isAdmin: false } }),
useMessages: () => ({
t: (value: string) => value,
labels: {
admin: 'Admin',
documentation: 'Documentation',
language: 'Language',
logout: 'Logout',
settings: 'Settings',
support: 'Support',
theme: 'Theme',
},
}),
useMobile: () => ({ isMobile: false }),
}));
test('renders the expanded sidebar control at full width and opens its menu', async () => {
const { user } = render(<UserButton />);
const button = screen.getByRole('button', { name: 'admin' });
expect(button).toHaveStyle({ width: '100%' });
await user.click(button);
expect(screen.getByRole('menuitem', { name: 'Settings' })).toBeInTheDocument();
});
test('keeps the collapsed sidebar control accessible and interactive', async () => {
const { user } = render(<UserButton showText={false} />);
const button = screen.getByRole('button', { name: 'admin' });
await user.click(button);
expect(screen.getByRole('menuitem', { name: 'Settings' })).toBeInTheDocument();
});
+25 -17
View File
@@ -1,12 +1,12 @@
import {
Button,
Column,
DialogTrigger,
Icon,
Menu,
MenuItem,
MenuSeparator,
MenuTrigger,
Popover,
Pressable,
Row,
SubmenuTrigger,
Text,
@@ -101,15 +101,19 @@ export function UserButton({ showText = true, onClose }: UserButtonProps) {
].filter(Boolean);
const trigger = (
<Pressable>
<Button
variant="zero"
aria-label={showText ? undefined : user.username}
style={{ padding: 0, width: '100%', justifyContent: 'flex-start' }}
>
<Row
alignItems="center"
flexGrow={1}
width="100%"
hover={{ backgroundColor: 'surface-sunken' }}
borderRadius
minHeight="40px"
role="button"
style={{ cursor: 'pointer', textWrap: 'nowrap', overflow: 'hidden', outline: 'none' }}
style={{ textWrap: 'nowrap', overflow: 'hidden' }}
>
<Row alignItems="center" gap padding>
<Icon>
@@ -118,19 +122,12 @@ export function UserButton({ showText = true, onClose }: UserButtonProps) {
{showText && <Text>{user.username}</Text>}
</Row>
</Row>
</Pressable>
</Button>
);
return (
<MenuTrigger>
{showText ? (
trigger
) : (
<TooltipTrigger delay={0}>
{trigger}
<Tooltip placement="right">{user.username}</Tooltip>
</TooltipTrigger>
)}
const menu = (
<DialogTrigger>
{trigger}
<Popover side="top" align="start">
<Column minWidth="200px">
<Menu>
@@ -224,6 +221,17 @@ export function UserButton({ showText = true, onClose }: UserButtonProps) {
</Menu>
</Column>
</Popover>
</MenuTrigger>
</DialogTrigger>
);
if (showText) {
return menu;
}
return (
<TooltipTrigger delay={0}>
<Row width="100%">{menu}</Row>
<Tooltip placement="right">{user.username}</Tooltip>
</TooltipTrigger>
);
}