Skip to content

Commit

Permalink
⌨️ a11y(Settings): Improved Keyboard Navigation & Consistent Styling (#…
Browse files Browse the repository at this point in the history
…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 <[email protected]>
  • Loading branch information
berry-13 and danny-avila authored Sep 10, 2024
1 parent 1a1e685 commit d6c0121
Show file tree
Hide file tree
Showing 17 changed files with 495 additions and 501 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,13 @@ export default function ConvoOptions({
id="conversation-menu-button"
aria-label={localize('com_nav_convo_menu_options')}
className={cn(
'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',
'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',
isActiveConvo === true
? 'opacity-100'
: 'opacity-0 focus:opacity-100 group-focus-within:opacity-100 group-hover:opacity-100 data-[open]:opacity-100',
)}
>
<Ellipsis className="icon-md text-text-secondary" aria-hidden={true}/>
<Ellipsis className="icon-md text-text-secondary" aria-hidden={true} />
</Ariakit.MenuButton>
}
items={dropdownItems}
Expand Down
222 changes: 112 additions & 110 deletions client/src/components/Nav/Settings.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as React from 'react';
import * as Tabs from '@radix-ui/react-tabs';
import { MessageSquare, Command } from 'lucide-react';
import { SettingsTabValues } from 'librechat-data-provider';
Expand All @@ -11,10 +12,45 @@ import { cn } from '~/utils';
export default function Settings({ open, onOpenChange }: TDialogProps) {
const isSmallScreen = useMediaQuery('(max-width: 767px)');
const localize = useLocalize();
const [activeTab, setActiveTab] = React.useState(SettingsTabValues.GENERAL);

const handleKeyDown = (event: React.KeyboardEvent) => {
const tabs = [
SettingsTabValues.GENERAL,
SettingsTabValues.CHAT,
SettingsTabValues.BETA,
SettingsTabValues.COMMANDS,
SettingsTabValues.SPEECH,
SettingsTabValues.DATA,
SettingsTabValues.ACCOUNT,
];
const currentIndex = tabs.indexOf(activeTab);

switch (event.key) {
case 'ArrowDown':
case 'ArrowRight':
event.preventDefault();
setActiveTab(tabs[(currentIndex + 1) % tabs.length]);
break;
case 'ArrowUp':
case 'ArrowLeft':
event.preventDefault();
setActiveTab(tabs[(currentIndex - 1 + tabs.length) % tabs.length]);
break;
case 'Home':
event.preventDefault();
setActiveTab(tabs[0]);
break;
case 'End':
event.preventDefault();
setActiveTab(tabs[tabs.length - 1]);
break;
}
};

return (
<Transition appear show={open}>
<Dialog as="div" className="relative z-50 focus:outline-none" onClose={onOpenChange}>
<Dialog as="div" className="relative z-50" onClose={onOpenChange}>
<TransitionChild
enter="ease-out duration-200"
enterFrom="opacity-0"
Expand Down Expand Up @@ -77,127 +113,93 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
</DialogTitle>
<div className="max-h-[373px] overflow-auto px-6 md:min-h-[373px] md:w-[680px]">
<Tabs.Root
defaultValue={SettingsTabValues.GENERAL}
value={activeTab}
onValueChange={(value: string) => setActiveTab(value as SettingsTabValues)}
className="flex flex-col gap-10 md:flex-row"
orientation="horizontal"
>
<Tabs.List
aria-label="Settings"
role="tablist"
aria-orientation="horizontal"
className={cn(
'min-w-auto max-w-auto -ml-[8px] flex flex-shrink-0 flex-col flex-nowrap overflow-auto sm:max-w-none',
isSmallScreen ? 'flex-row rounded-lg bg-surface-secondary' : '',
)}
style={{ outline: 'none' }}
onKeyDown={handleKeyDown}
>
<Tabs.Trigger
tabIndex={0}
className={cn(
'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',
isSmallScreen
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
: 'bg-surface-tertiary-alt',
)}
value={SettingsTabValues.GENERAL}
style={{ userSelect: 'none' }}
>
<GearIcon />
{localize('com_nav_setting_general')}
</Tabs.Trigger>
<Tabs.Trigger
tabIndex={0}
className={cn(
'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',
isSmallScreen
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
: 'bg-surface-tertiary-alt',
)}
value={SettingsTabValues.CHAT}
style={{ userSelect: 'none' }}
>
<MessageSquare className="icon-sm" />
{localize('com_nav_setting_chat')}
</Tabs.Trigger>
<Tabs.Trigger
tabIndex={0}
className={cn(
'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',
isSmallScreen
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
: 'bg-surface-tertiary-alt',
)}
value={SettingsTabValues.BETA}
style={{ userSelect: 'none' }}
>
<ExperimentIcon />
{localize('com_nav_setting_beta')}
</Tabs.Trigger>
<Tabs.Trigger
tabIndex={0}
className={cn(
'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',
isSmallScreen
? 'flex-1 items-center justify-center text-nowrap text-sm text-text-secondary'
: 'bg-surface-tertiary-alt',
)}
value={SettingsTabValues.COMMANDS}
style={{ userSelect: 'none' }}
>
<Command className="icon-sm" />
{localize('com_nav_commands')}
</Tabs.Trigger>
<Tabs.Trigger
tabIndex={0}
className={cn(
'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',
isSmallScreen
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
: 'bg-surface-tertiary-alt',
)}
value={SettingsTabValues.SPEECH}
style={{ userSelect: 'none' }}
>
<SpeechIcon className="icon-sm" />
{localize('com_nav_setting_speech')}
</Tabs.Trigger>
<Tabs.Trigger
tabIndex={0}
className={cn(
'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',
isSmallScreen
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
: 'bg-surface-tertiary-alt',
)}
value={SettingsTabValues.DATA}
style={{ userSelect: 'none' }}
>
<DataIcon />
{localize('com_nav_setting_data')}
</Tabs.Trigger>
<Tabs.Trigger
tabIndex={0}
className={cn(
'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',
isSmallScreen
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
: 'bg-surface-tertiary-alt',
)}
value={SettingsTabValues.ACCOUNT}
style={{ userSelect: 'none' }}
>
<UserIcon />
{localize('com_nav_setting_account')}
</Tabs.Trigger>
{[
{
value: SettingsTabValues.GENERAL,
icon: <GearIcon />,
label: 'com_nav_setting_general',
},
{
value: SettingsTabValues.CHAT,
icon: <MessageSquare className="icon-sm" />,
label: 'com_nav_setting_chat',
},
{
value: SettingsTabValues.BETA,
icon: <ExperimentIcon />,
label: 'com_nav_setting_beta',
},
{
value: SettingsTabValues.COMMANDS,
icon: <Command className="icon-sm" />,
label: 'com_nav_commands',
},
{
value: SettingsTabValues.SPEECH,
icon: <SpeechIcon className="icon-sm" />,
label: 'com_nav_setting_speech',
},
{
value: SettingsTabValues.DATA,
icon: <DataIcon />,
label: 'com_nav_setting_data',
},
{
value: SettingsTabValues.ACCOUNT,
icon: <UserIcon />,
label: 'com_nav_setting_account',
},
].map(({ value, icon, label }) => (
<Tabs.Trigger
key={value}
className={cn(
'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',
isSmallScreen
? 'flex-1 items-center justify-center text-nowrap p-1 px-3 text-sm text-text-secondary'
: 'bg-surface-tertiary-alt',
)}
value={value}
>
{icon}
{localize(label)}
</Tabs.Trigger>
))}
</Tabs.List>
<div className="max-h-[373px] overflow-auto sm:w-full sm:max-w-none md:pr-0.5 md:pt-0.5">
<General />
<Chat />
<Beta />
<Commands />
<Speech />
<Data />
<Account />
<Tabs.Content value={SettingsTabValues.GENERAL}>
<General />
</Tabs.Content>
<Tabs.Content value={SettingsTabValues.CHAT}>
<Chat />
</Tabs.Content>
<Tabs.Content value={SettingsTabValues.BETA}>
<Beta />
</Tabs.Content>
<Tabs.Content value={SettingsTabValues.COMMANDS}>
<Commands />
</Tabs.Content>
<Tabs.Content value={SettingsTabValues.SPEECH}>
<Speech />
</Tabs.Content>
<Tabs.Content value={SettingsTabValues.DATA}>
<Data />
</Tabs.Content>
<Tabs.Content value={SettingsTabValues.ACCOUNT}>
<Account />
</Tabs.Content>
</div>
</Tabs.Root>
</div>
Expand Down
46 changes: 19 additions & 27 deletions client/src/components/Nav/SettingsTabs/Account/Account.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import React from 'react';
import { useRecoilState } from 'recoil';
import * as Tabs from '@radix-ui/react-tabs';
import { SettingsTabValues } from 'librechat-data-provider';
import HoverCardSettings from '../HoverCardSettings';
import DeleteAccount from './DeleteAccount';
import { Switch } from '~/components/ui';
Expand All @@ -21,33 +19,27 @@ function Account({ onCheckedChange }: { onCheckedChange?: (value: boolean) => vo
};

return (
<Tabs.Content
value={SettingsTabValues.ACCOUNT}
role="tabpanel"
className="w-full md:min-h-[271px]"
>
<div className="flex flex-col gap-3 text-sm text-black dark:text-gray-50">
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
<Avatar />
</div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
<DeleteAccount />
</div>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<div>{localize('com_nav_user_name_display')}</div>
<HoverCardSettings side="bottom" text="com_nav_info_user_name_display" />
</div>
<Switch
id="UsernameDisplay"
checked={UsernameDisplay}
onCheckedChange={handleCheckedChange}
className="ml-4 mt-2"
data-testid="UsernameDisplay"
/>
<div className="flex flex-col gap-3 p-1 text-sm text-text-primary">
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
<Avatar />
</div>
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
<DeleteAccount />
</div>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<div>{localize('com_nav_user_name_display')}</div>
<HoverCardSettings side="bottom" text="com_nav_info_user_name_display" />
</div>
<Switch
id="UsernameDisplay"
checked={UsernameDisplay}
onCheckedChange={handleCheckedChange}
className="ml-4 mt-2"
data-testid="UsernameDisplay"
/>
</div>
</Tabs.Content>
</div>
);
}

Expand Down
Loading

0 comments on commit d6c0121

Please sign in to comment.