Skip to content

Commit d6c0121

Browse files
⌨️ a11y(Settings): Improved Keyboard Navigation & Consistent Styling (#3975)
* feat: settings tba accessible * refactor: cleanup unused code * refactor: improve accessibility and user experience in ChatDirection component * style: focus ring primary class * improve a11y of avatar dialog * style: a11y improvements for Settings * style: focus ring primary class in OriginalDialog component --------- Co-authored-by: Danny Avila <danny@librechat.ai>
1 parent 1a1e685 commit d6c0121

File tree

17 files changed

+495
-501
lines changed

17 files changed

+495
-501
lines changed

client/src/components/Conversations/ConvoOptions/ConvoOptions.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,13 @@ export default function ConvoOptions({
7070
id="conversation-menu-button"
7171
aria-label={localize('com_nav_convo_menu_options')}
7272
className={cn(
73-
'z-30 inline-flex h-7 w-7 items-center justify-center gap-2 rounded-md border-none p-0 text-sm font-medium transition-all duration-200 ease-in-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
73+
'z-30 inline-flex h-7 w-7 items-center justify-center gap-2 rounded-md border-none p-0 text-sm font-medium ring-ring-primary transition-all duration-200 ease-in-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
7474
isActiveConvo === true
7575
? 'opacity-100'
7676
: 'opacity-0 focus:opacity-100 group-focus-within:opacity-100 group-hover:opacity-100 data-[open]:opacity-100',
7777
)}
7878
>
79-
<Ellipsis className="icon-md text-text-secondary" aria-hidden={true}/>
79+
<Ellipsis className="icon-md text-text-secondary" aria-hidden={true} />
8080
</Ariakit.MenuButton>
8181
}
8282
items={dropdownItems}

client/src/components/Nav/Settings.tsx

Lines changed: 112 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as React from 'react';
12
import * as Tabs from '@radix-ui/react-tabs';
23
import { MessageSquare, Command } from 'lucide-react';
34
import { SettingsTabValues } from 'librechat-data-provider';
@@ -11,10 +12,45 @@ import { cn } from '~/utils';
1112
export default function Settings({ open, onOpenChange }: TDialogProps) {
1213
const isSmallScreen = useMediaQuery('(max-width: 767px)');
1314
const localize = useLocalize();
15+
const [activeTab, setActiveTab] = React.useState(SettingsTabValues.GENERAL);
16+
17+
const handleKeyDown = (event: React.KeyboardEvent) => {
18+
const tabs = [
19+
SettingsTabValues.GENERAL,
20+
SettingsTabValues.CHAT,
21+
SettingsTabValues.BETA,
22+
SettingsTabValues.COMMANDS,
23+
SettingsTabValues.SPEECH,
24+
SettingsTabValues.DATA,
25+
SettingsTabValues.ACCOUNT,
26+
];
27+
const currentIndex = tabs.indexOf(activeTab);
28+
29+
switch (event.key) {
30+
case 'ArrowDown':
31+
case 'ArrowRight':
32+
event.preventDefault();
33+
setActiveTab(tabs[(currentIndex + 1) % tabs.length]);
34+
break;
35+
case 'ArrowUp':
36+
case 'ArrowLeft':
37+
event.preventDefault();
38+
setActiveTab(tabs[(currentIndex - 1 + tabs.length) % tabs.length]);
39+
break;
40+
case 'Home':
41+
event.preventDefault();
42+
setActiveTab(tabs[0]);
43+
break;
44+
case 'End':
45+
event.preventDefault();
46+
setActiveTab(tabs[tabs.length - 1]);
47+
break;
48+
}
49+
};
1450

1551
return (
1652
<Transition appear show={open}>
17-
<Dialog as="div" className="relative z-50 focus:outline-none" onClose={onOpenChange}>
53+
<Dialog as="div" className="relative z-50" onClose={onOpenChange}>
1854
<TransitionChild
1955
enter="ease-out duration-200"
2056
enterFrom="opacity-0"
@@ -77,127 +113,93 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
77113
</DialogTitle>
78114
<div className="max-h-[373px] overflow-auto px-6 md:min-h-[373px] md:w-[680px]">
79115
<Tabs.Root
80-
defaultValue={SettingsTabValues.GENERAL}
116+
value={activeTab}
117+
onValueChange={(value: string) => setActiveTab(value as SettingsTabValues)}
81118
className="flex flex-col gap-10 md:flex-row"
82119
orientation="horizontal"
83120
>
84121
<Tabs.List
85122
aria-label="Settings"
86-
role="tablist"
87-
aria-orientation="horizontal"
88123
className={cn(
89124
'min-w-auto max-w-auto -ml-[8px] flex flex-shrink-0 flex-col flex-nowrap overflow-auto sm:max-w-none',
90125
isSmallScreen ? 'flex-row rounded-lg bg-surface-secondary' : '',
91126
)}
92-
style={{ outline: 'none' }}
127+
onKeyDown={handleKeyDown}
93128
>
94-
<Tabs.Trigger
95-
tabIndex={0}
96-
className={cn(
97-
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-active',
98-
isSmallScreen
99-
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
100-
: 'bg-surface-tertiary-alt',
101-
)}
102-
value={SettingsTabValues.GENERAL}
103-
style={{ userSelect: 'none' }}
104-
>
105-
<GearIcon />
106-
{localize('com_nav_setting_general')}
107-
</Tabs.Trigger>
108-
<Tabs.Trigger
109-
tabIndex={0}
110-
className={cn(
111-
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-active',
112-
isSmallScreen
113-
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
114-
: 'bg-surface-tertiary-alt',
115-
)}
116-
value={SettingsTabValues.CHAT}
117-
style={{ userSelect: 'none' }}
118-
>
119-
<MessageSquare className="icon-sm" />
120-
{localize('com_nav_setting_chat')}
121-
</Tabs.Trigger>
122-
<Tabs.Trigger
123-
tabIndex={0}
124-
className={cn(
125-
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-active',
126-
isSmallScreen
127-
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
128-
: 'bg-surface-tertiary-alt',
129-
)}
130-
value={SettingsTabValues.BETA}
131-
style={{ userSelect: 'none' }}
132-
>
133-
<ExperimentIcon />
134-
{localize('com_nav_setting_beta')}
135-
</Tabs.Trigger>
136-
<Tabs.Trigger
137-
tabIndex={0}
138-
className={cn(
139-
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-active',
140-
isSmallScreen
141-
? 'flex-1 items-center justify-center text-nowrap text-sm text-text-secondary'
142-
: 'bg-surface-tertiary-alt',
143-
)}
144-
value={SettingsTabValues.COMMANDS}
145-
style={{ userSelect: 'none' }}
146-
>
147-
<Command className="icon-sm" />
148-
{localize('com_nav_commands')}
149-
</Tabs.Trigger>
150-
<Tabs.Trigger
151-
tabIndex={0}
152-
className={cn(
153-
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-active',
154-
isSmallScreen
155-
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
156-
: 'bg-surface-tertiary-alt',
157-
)}
158-
value={SettingsTabValues.SPEECH}
159-
style={{ userSelect: 'none' }}
160-
>
161-
<SpeechIcon className="icon-sm" />
162-
{localize('com_nav_setting_speech')}
163-
</Tabs.Trigger>
164-
<Tabs.Trigger
165-
tabIndex={0}
166-
className={cn(
167-
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-active',
168-
isSmallScreen
169-
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
170-
: 'bg-surface-tertiary-alt',
171-
)}
172-
value={SettingsTabValues.DATA}
173-
style={{ userSelect: 'none' }}
174-
>
175-
<DataIcon />
176-
{localize('com_nav_setting_data')}
177-
</Tabs.Trigger>
178-
<Tabs.Trigger
179-
tabIndex={0}
180-
className={cn(
181-
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-active',
182-
isSmallScreen
183-
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
184-
: 'bg-surface-tertiary-alt',
185-
)}
186-
value={SettingsTabValues.ACCOUNT}
187-
style={{ userSelect: 'none' }}
188-
>
189-
<UserIcon />
190-
{localize('com_nav_setting_account')}
191-
</Tabs.Trigger>
129+
{[
130+
{
131+
value: SettingsTabValues.GENERAL,
132+
icon: <GearIcon />,
133+
label: 'com_nav_setting_general',
134+
},
135+
{
136+
value: SettingsTabValues.CHAT,
137+
icon: <MessageSquare className="icon-sm" />,
138+
label: 'com_nav_setting_chat',
139+
},
140+
{
141+
value: SettingsTabValues.BETA,
142+
icon: <ExperimentIcon />,
143+
label: 'com_nav_setting_beta',
144+
},
145+
{
146+
value: SettingsTabValues.COMMANDS,
147+
icon: <Command className="icon-sm" />,
148+
label: 'com_nav_commands',
149+
},
150+
{
151+
value: SettingsTabValues.SPEECH,
152+
icon: <SpeechIcon className="icon-sm" />,
153+
label: 'com_nav_setting_speech',
154+
},
155+
{
156+
value: SettingsTabValues.DATA,
157+
icon: <DataIcon />,
158+
label: 'com_nav_setting_data',
159+
},
160+
{
161+
value: SettingsTabValues.ACCOUNT,
162+
icon: <UserIcon />,
163+
label: 'com_nav_setting_account',
164+
},
165+
].map(({ value, icon, label }) => (
166+
<Tabs.Trigger
167+
key={value}
168+
className={cn(
169+
'group m-1 flex items-center justify-start gap-2 rounded-md px-2 py-1.5 text-sm text-text-primary transition-all duration-200 ease-in-out radix-state-active:bg-surface-tertiary radix-state-active:text-text-primary dark:radix-state-active:bg-surface-active',
170+
isSmallScreen
171+
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
172+
: 'bg-surface-tertiary-alt',
173+
)}
174+
value={value}
175+
>
176+
{icon}
177+
{localize(label)}
178+
</Tabs.Trigger>
179+
))}
192180
</Tabs.List>
193181
<div className="max-h-[373px] overflow-auto sm:w-full sm:max-w-none md:pr-0.5 md:pt-0.5">
194-
<General />
195-
<Chat />
196-
<Beta />
197-
<Commands />
198-
<Speech />
199-
<Data />
200-
<Account />
182+
<Tabs.Content value={SettingsTabValues.GENERAL}>
183+
<General />
184+
</Tabs.Content>
185+
<Tabs.Content value={SettingsTabValues.CHAT}>
186+
<Chat />
187+
</Tabs.Content>
188+
<Tabs.Content value={SettingsTabValues.BETA}>
189+
<Beta />
190+
</Tabs.Content>
191+
<Tabs.Content value={SettingsTabValues.COMMANDS}>
192+
<Commands />
193+
</Tabs.Content>
194+
<Tabs.Content value={SettingsTabValues.SPEECH}>
195+
<Speech />
196+
</Tabs.Content>
197+
<Tabs.Content value={SettingsTabValues.DATA}>
198+
<Data />
199+
</Tabs.Content>
200+
<Tabs.Content value={SettingsTabValues.ACCOUNT}>
201+
<Account />
202+
</Tabs.Content>
201203
</div>
202204
</Tabs.Root>
203205
</div>

client/src/components/Nav/SettingsTabs/Account/Account.tsx

Lines changed: 19 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import React from 'react';
22
import { useRecoilState } from 'recoil';
3-
import * as Tabs from '@radix-ui/react-tabs';
4-
import { SettingsTabValues } from 'librechat-data-provider';
53
import HoverCardSettings from '../HoverCardSettings';
64
import DeleteAccount from './DeleteAccount';
75
import { Switch } from '~/components/ui';
@@ -21,33 +19,27 @@ function Account({ onCheckedChange }: { onCheckedChange?: (value: boolean) => vo
2119
};
2220

2321
return (
24-
<Tabs.Content
25-
value={SettingsTabValues.ACCOUNT}
26-
role="tabpanel"
27-
className="w-full md:min-h-[271px]"
28-
>
29-
<div className="flex flex-col gap-3 text-sm text-black dark:text-gray-50">
30-
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
31-
<Avatar />
32-
</div>
33-
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
34-
<DeleteAccount />
35-
</div>
36-
<div className="flex items-center justify-between">
37-
<div className="flex items-center space-x-2">
38-
<div>{localize('com_nav_user_name_display')}</div>
39-
<HoverCardSettings side="bottom" text="com_nav_info_user_name_display" />
40-
</div>
41-
<Switch
42-
id="UsernameDisplay"
43-
checked={UsernameDisplay}
44-
onCheckedChange={handleCheckedChange}
45-
className="ml-4 mt-2"
46-
data-testid="UsernameDisplay"
47-
/>
22+
<div className="flex flex-col gap-3 p-1 text-sm text-text-primary">
23+
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
24+
<Avatar />
25+
</div>
26+
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
27+
<DeleteAccount />
28+
</div>
29+
<div className="flex items-center justify-between">
30+
<div className="flex items-center space-x-2">
31+
<div>{localize('com_nav_user_name_display')}</div>
32+
<HoverCardSettings side="bottom" text="com_nav_info_user_name_display" />
4833
</div>
34+
<Switch
35+
id="UsernameDisplay"
36+
checked={UsernameDisplay}
37+
onCheckedChange={handleCheckedChange}
38+
className="ml-4 mt-2"
39+
data-testid="UsernameDisplay"
40+
/>
4941
</div>
50-
</Tabs.Content>
42+
</div>
5143
);
5244
}
5345

0 commit comments

Comments
 (0)