From d6c0121b1961f3237294a3f0cc313925a165826e Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Tue, 10 Sep 2024 10:11:39 -0900 Subject: [PATCH] =?UTF-8?q?=E2=8C=A8=EF=B8=8F=20a11y(Settings):=20Improved?= =?UTF-8?q?=20Keyboard=20Navigation=20&=20Consistent=20Styling=20(#3975)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- .../ConvoOptions/ConvoOptions.tsx | 4 +- client/src/components/Nav/Settings.tsx | 222 ++++++++--------- .../Nav/SettingsTabs/Account/Account.tsx | 46 ++-- .../Nav/SettingsTabs/Account/Avatar.tsx | 201 ++++++++-------- .../components/Nav/SettingsTabs/Beta/Beta.tsx | 16 +- .../components/Nav/SettingsTabs/Chat/Chat.tsx | 50 ++-- .../Nav/SettingsTabs/Chat/ChatDirection.tsx | 18 +- .../Nav/SettingsTabs/Commands/Commands.tsx | 47 ++-- .../components/Nav/SettingsTabs/Data/Data.tsx | 51 ++-- .../SettingsTabs/Data/ImportConversations.tsx | 50 ++-- .../SettingsTabs/General/AutoScrollSwitch.tsx | 2 +- .../Nav/SettingsTabs/General/General.tsx | 45 ++-- .../Nav/SettingsTabs/Speech/Speech.tsx | 224 +++++++++--------- client/src/components/ui/Dropdown.tsx | 4 +- client/src/components/ui/OriginalDialog.tsx | 2 +- client/src/components/ui/Switch.tsx | 1 + client/src/style.css | 13 + 17 files changed, 495 insertions(+), 501 deletions(-) diff --git a/client/src/components/Conversations/ConvoOptions/ConvoOptions.tsx b/client/src/components/Conversations/ConvoOptions/ConvoOptions.tsx index 3b3dcb40b37..60de8fd726e 100644 --- a/client/src/components/Conversations/ConvoOptions/ConvoOptions.tsx +++ b/client/src/components/Conversations/ConvoOptions/ConvoOptions.tsx @@ -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', )} > - + } items={dropdownItems} diff --git a/client/src/components/Nav/Settings.tsx b/client/src/components/Nav/Settings.tsx index fc3f27ed02f..09e473d3cbd 100644 --- a/client/src/components/Nav/Settings.tsx +++ b/client/src/components/Nav/Settings.tsx @@ -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'; @@ -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 ( - +
setActiveTab(value as SettingsTabValues)} className="flex flex-col gap-10 md:flex-row" orientation="horizontal" > - - - {localize('com_nav_setting_general')} - - - - {localize('com_nav_setting_chat')} - - - - {localize('com_nav_setting_beta')} - - - - {localize('com_nav_commands')} - - - - {localize('com_nav_setting_speech')} - - - - {localize('com_nav_setting_data')} - - - - {localize('com_nav_setting_account')} - + {[ + { + value: SettingsTabValues.GENERAL, + icon: , + label: 'com_nav_setting_general', + }, + { + value: SettingsTabValues.CHAT, + icon: , + label: 'com_nav_setting_chat', + }, + { + value: SettingsTabValues.BETA, + icon: , + label: 'com_nav_setting_beta', + }, + { + value: SettingsTabValues.COMMANDS, + icon: , + label: 'com_nav_commands', + }, + { + value: SettingsTabValues.SPEECH, + icon: , + label: 'com_nav_setting_speech', + }, + { + value: SettingsTabValues.DATA, + icon: , + label: 'com_nav_setting_data', + }, + { + value: SettingsTabValues.ACCOUNT, + icon: , + label: 'com_nav_setting_account', + }, + ].map(({ value, icon, label }) => ( + + {icon} + {localize(label)} + + ))}
- - - - - - - + + + + + + + + + + + + + + + + + + + + +
diff --git a/client/src/components/Nav/SettingsTabs/Account/Account.tsx b/client/src/components/Nav/SettingsTabs/Account/Account.tsx index 6227f3c7a3f..a0fd53445eb 100644 --- a/client/src/components/Nav/SettingsTabs/Account/Account.tsx +++ b/client/src/components/Nav/SettingsTabs/Account/Account.tsx @@ -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'; @@ -21,33 +19,27 @@ function Account({ onCheckedChange }: { onCheckedChange?: (value: boolean) => vo }; return ( - -
-
- -
-
- -
-
-
-
{localize('com_nav_user_name_display')}
- -
- +
+
+ +
+
+ +
+
+
+
{localize('com_nav_user_name_display')}
+
+
- +
); } diff --git a/client/src/components/Nav/SettingsTabs/Account/Avatar.tsx b/client/src/components/Nav/SettingsTabs/Account/Avatar.tsx index 95ac3d464f1..eb04d3f068d 100644 --- a/client/src/components/Nav/SettingsTabs/Account/Avatar.tsx +++ b/client/src/components/Nav/SettingsTabs/Account/Avatar.tsx @@ -4,7 +4,14 @@ import { useSetRecoilState } from 'recoil'; import AvatarEditor from 'react-avatar-editor'; import { fileConfig as defaultFileConfig, mergeFileConfig } from 'librechat-data-provider'; import type { TUser } from 'librechat-data-provider'; -import { Dialog, DialogContent, DialogHeader, DialogTitle, Slider } from '~/components/ui'; +import { + OGDialog, + OGDialogContent, + OGDialogHeader, + OGDialogTitle, + OGDialogTrigger, + Slider, +} from '~/components/ui'; import { useUploadAvatarMutation, useGetFileConfig } from '~/data-provider'; import { useToastContext } from '~/Providers'; import { Spinner } from '~/components/svg'; @@ -20,6 +27,7 @@ function Avatar() { const [rotation, setRotation] = useState(0); const editorRef = useRef(null); const fileInputRef = useRef(null); + const openButtonRef = useRef(null); const { data: fileConfig = defaultFileConfig } = useGetFileConfig({ select: (data) => mergeFileConfig(data), @@ -31,8 +39,8 @@ function Avatar() { const { mutate: uploadAvatar, isLoading: isUploading } = useUploadAvatarMutation({ onSuccess: (data) => { showToast({ message: localize('com_ui_upload_success') }); - setDialogOpen(false); setUser((prev) => ({ ...prev, avatar: data.url } as TUser)); + openButtonRef.current?.click(); }, onError: (error) => { console.error('Error:', error); @@ -102,113 +110,114 @@ function Avatar() { }, []); return ( - <> + { + setDialogOpen(open); + if (!open) { + resetImage(); + setTimeout(() => { + openButtonRef.current?.focus(); + }, 0); + } + }} + >
{localize('com_nav_profile_picture')} - +
- { - setDialogOpen(open); - if (!open) { - resetImage(); - } - }} + - - - - {image ? localize('com_ui_preview') : localize('com_ui_upload_image')} - - -
- {image ? ( - <> -
- + + {image ? localize('com_ui_preview') : localize('com_ui_upload_image')} + + +
+ {image ? ( + <> +
+ +
+
+
+ Zoom: +
-
-
- Zoom: - -
- -
- - ) : ( -
- -

- {localize('com_ui_drag_drop')} -

- -
- )} -
- -
- + + + ) : ( +
+ +

+ {localize('com_ui_drag_drop')} +

+ + +
+ )} +
+ + ); } diff --git a/client/src/components/Nav/SettingsTabs/Beta/Beta.tsx b/client/src/components/Nav/SettingsTabs/Beta/Beta.tsx index e4aae59dd6d..c71c11d4adb 100644 --- a/client/src/components/Nav/SettingsTabs/Beta/Beta.tsx +++ b/client/src/components/Nav/SettingsTabs/Beta/Beta.tsx @@ -1,21 +1,13 @@ import { memo } from 'react'; -import * as Tabs from '@radix-ui/react-tabs'; -import { SettingsTabValues } from 'librechat-data-provider'; import CodeArtifacts from './CodeArtifacts'; function Beta() { return ( - -
-
- -
+
+
+
- +
); } diff --git a/client/src/components/Nav/SettingsTabs/Chat/Chat.tsx b/client/src/components/Nav/SettingsTabs/Chat/Chat.tsx index d7278e6c3cf..94d9eb64995 100644 --- a/client/src/components/Nav/SettingsTabs/Chat/Chat.tsx +++ b/client/src/components/Nav/SettingsTabs/Chat/Chat.tsx @@ -1,6 +1,4 @@ import { memo } from 'react'; -import * as Tabs from '@radix-ui/react-tabs'; -import { SettingsTabValues } from 'librechat-data-provider'; import FontSizeSelector from './FontSizeSelector'; import SendMessageKeyEnter from './EnterToSend'; import ShowCodeSwitch from './ShowCodeSwitch'; @@ -12,32 +10,30 @@ import SaveDraft from './SaveDraft'; function Chat() { return ( - -
-
- -
-
- -
-
- -
-
- -
-
- -
- -
- -
-
- -
+
+
+
- +
+ +
+
+ +
+
+ +
+
+ +
+ +
+ +
+
+ +
+
); } diff --git a/client/src/components/Nav/SettingsTabs/Chat/ChatDirection.tsx b/client/src/components/Nav/SettingsTabs/Chat/ChatDirection.tsx index c5fdcaa07a7..6048cb05076 100644 --- a/client/src/components/Nav/SettingsTabs/Chat/ChatDirection.tsx +++ b/client/src/components/Nav/SettingsTabs/Chat/ChatDirection.tsx @@ -14,16 +14,22 @@ const ChatDirection = () => { return (
- {localize('com_nav_chat_direction')} + {localize('com_nav_chat_direction')}
- + + + {direction === 'LTR' + ? localize('chat_direction_left_to_right') + : localize('chat_direction_right_to_left')} + +
); }; diff --git a/client/src/components/Nav/SettingsTabs/Commands/Commands.tsx b/client/src/components/Nav/SettingsTabs/Commands/Commands.tsx index bbec995aa3d..d9c33034c28 100644 --- a/client/src/components/Nav/SettingsTabs/Commands/Commands.tsx +++ b/client/src/components/Nav/SettingsTabs/Commands/Commands.tsx @@ -1,6 +1,5 @@ import { memo } from 'react'; -import * as Tabs from '@radix-ui/react-tabs'; -import { SettingsTabValues, PermissionTypes, Permissions } from 'librechat-data-provider'; +import { PermissionTypes, Permissions } from 'librechat-data-provider'; import HoverCardSettings from '~/components/Nav/SettingsTabs/HoverCardSettings'; import { useLocalize, useHasAccess } from '~/hooks'; import SlashCommandSwitch from './SlashCommandSwitch'; @@ -21,35 +20,29 @@ function Commands() { }); return ( - -
-
-

- {localize('com_nav_chat_commands')} -

- +
+
+

+ {localize('com_nav_chat_commands')} +

+ +
+
+
+
-
+ {hasAccessToMultiConvo === true && (
- +
- {hasAccessToMultiConvo === true && ( -
- -
- )} - {hasAccessToPrompts === true && ( -
- -
- )} -
+ )} + {hasAccessToPrompts === true && ( +
+ +
+ )}
- +
); } diff --git a/client/src/components/Nav/SettingsTabs/Data/Data.tsx b/client/src/components/Nav/SettingsTabs/Data/Data.tsx index f7576b117f8..6897a4a0b40 100644 --- a/client/src/components/Nav/SettingsTabs/Data/Data.tsx +++ b/client/src/components/Nav/SettingsTabs/Data/Data.tsx @@ -1,7 +1,5 @@ import React, { useState, useRef } from 'react'; -import * as Tabs from '@radix-ui/react-tabs'; import { useClearConversationsMutation } from 'librechat-data-provider/react-query'; -import { SettingsTabValues } from 'librechat-data-provider'; import { useConversation, useConversations, useOnClickOutside } from '~/hooks'; import { RevokeKeysButton } from './RevokeKeysButton'; import { DeleteCacheButton } from './DeleteCacheButton'; @@ -37,35 +35,28 @@ function Data() { }; return ( - -
-
- -
-
- -
-
- -
-
- -
-
- -
+
+
+
- +
+ +
+
+ +
+
+ +
+
+ +
+
); } diff --git a/client/src/components/Nav/SettingsTabs/Data/ImportConversations.tsx b/client/src/components/Nav/SettingsTabs/Data/ImportConversations.tsx index a75eaddd264..7b318e6495e 100644 --- a/client/src/components/Nav/SettingsTabs/Data/ImportConversations.tsx +++ b/client/src/components/Nav/SettingsTabs/Data/ImportConversations.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useRef } from 'react'; import { Import } from 'lucide-react'; import type { TError } from 'librechat-data-provider'; import { useUploadConversationsMutation } from '~/data-provider'; @@ -9,6 +9,7 @@ import { cn } from '~/utils'; function ImportConversations() { const localize = useLocalize(); + const fileInputRef = useRef(null); const { showToast } = useToastContext(); const [, setErrors] = useState([]); @@ -26,7 +27,7 @@ function ImportConversations() { console.error('Error: ', error); setAllowImport(true); setError( - (error as TError)?.response?.data?.message ?? 'An error occurred while uploading the file.', + (error as TError).response?.data?.message ?? 'An error occurred while uploading the file.', ); if (error?.toString().includes('Unsupported import type')) { showToast({ @@ -44,13 +45,12 @@ function ImportConversations() { const startUpload = async (file: File) => { const formData = new FormData(); - formData.append('file', file, encodeURIComponent(file?.name || 'File')); + formData.append('file', file, encodeURIComponent(file.name || 'File')); uploadFile.mutate(formData); }; const handleFiles = async (_file: File) => { - /* Process files */ try { await startUpload(_file); } catch (error) { @@ -59,33 +59,49 @@ function ImportConversations() { } }; - const handleFileChange = (event) => { - const file = event.target.files[0]; + const handleFileChange = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; if (file) { handleFiles(file); } }; + const handleImportClick = () => { + fileInputRef.current?.click(); + }; + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + handleImportClick(); + } + }; + return (
{localize('com_ui_import_conversation_info')}
-
); } diff --git a/client/src/components/Nav/SettingsTabs/General/AutoScrollSwitch.tsx b/client/src/components/Nav/SettingsTabs/General/AutoScrollSwitch.tsx index 8eb99e571b9..e811e3e7fe2 100644 --- a/client/src/components/Nav/SettingsTabs/General/AutoScrollSwitch.tsx +++ b/client/src/components/Nav/SettingsTabs/General/AutoScrollSwitch.tsx @@ -25,7 +25,7 @@ export default function AutoScrollSwitch({ id="autoScroll" checked={autoScroll} onCheckedChange={handleCheckedChange} - className="ml-4 mt-2" + className="ml-4 mt-2 ring-ring-primary" data-testid="autoScroll" />
diff --git a/client/src/components/Nav/SettingsTabs/General/General.tsx b/client/src/components/Nav/SettingsTabs/General/General.tsx index 7a01e984199..66aed110b2e 100644 --- a/client/src/components/Nav/SettingsTabs/General/General.tsx +++ b/client/src/components/Nav/SettingsTabs/General/General.tsx @@ -1,7 +1,5 @@ import { useRecoilState } from 'recoil'; -import * as Tabs from '@radix-ui/react-tabs'; import Cookies from 'js-cookie'; -import { SettingsTabValues } from 'librechat-data-provider'; import React, { useContext, useCallback, useRef } from 'react'; import type { TDangerButtonProps } from '~/common'; import { ThemeContext, useLocalize } from '~/hooks'; @@ -151,32 +149,25 @@ function General() { ); return ( - -
-
- -
-
- -
-
- -
-
- -
-
- -
- {/*
-
*/} +
+
+ +
+
+ +
+
+ +
+
+
- +
+ +
+ {/*
+
*/} +
); } diff --git a/client/src/components/Nav/SettingsTabs/Speech/Speech.tsx b/client/src/components/Nav/SettingsTabs/Speech/Speech.tsx index 779c32ef93b..d1420b89f43 100644 --- a/client/src/components/Nav/SettingsTabs/Speech/Speech.tsx +++ b/client/src/components/Nav/SettingsTabs/Speech/Speech.tsx @@ -1,7 +1,6 @@ import { useRecoilState } from 'recoil'; import * as Tabs from '@radix-ui/react-tabs'; import { Lightbulb, Cog } from 'lucide-react'; -import { SettingsTabValues } from 'librechat-data-provider'; import React, { useState, useRef, useEffect, useCallback } from 'react'; import { useGetCustomConfigSpeechQuery } from 'librechat-data-provider/react-query'; import { @@ -141,130 +140,123 @@ function Speech() { useOnClickOutside(contentRef, () => confirmClear && setConfirmClear(false), []); return ( - - -
- - setAdvancedMode(false)} - className={cn( - 'group m-1 flex items-center justify-center gap-2 rounded-md px-4 py-2 text-sm text-black transition-all duration-200 ease-in-out radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600', - isSmallScreen - ? 'flex-row items-center justify-center text-sm text-gray-700 radix-state-active:bg-gray-100 radix-state-active:text-black dark:text-gray-300 dark:radix-state-active:text-white' - : 'bg-white radix-state-active:bg-gray-100 dark:bg-gray-700', - 'w-full', - )} - value="simple" - style={{ userSelect: 'none' }} - > - - Simple - - setAdvancedMode(true)} - className={cn( - 'group m-1 flex items-center justify-center gap-2 rounded-md px-4 py-2 text-sm text-black transition-all duration-200 ease-in-out radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600', - isSmallScreen - ? 'flex-row items-center justify-center text-sm text-gray-700 radix-state-active:bg-gray-100 radix-state-active:text-black dark:text-gray-300 dark:radix-state-active:text-white' - : 'bg-white radix-state-active:bg-gray-100 dark:bg-gray-700', - 'w-full', - )} - value="advanced" - style={{ userSelect: 'none' }} - > - - Advanced - - -
+
+ + setAdvancedMode(false)} + className={cn( + 'group m-1 flex items-center justify-center gap-2 rounded-md px-4 py-2 text-sm text-black transition-all duration-200 ease-in-out radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600', + isSmallScreen + ? 'flex-row items-center justify-center text-sm text-gray-700 radix-state-active:bg-gray-100 radix-state-active:text-black dark:text-gray-300 dark:radix-state-active:text-white' + : 'bg-white radix-state-active:bg-gray-100 dark:bg-gray-700', + 'w-full', + )} + value="simple" + style={{ userSelect: 'none' }} + > + + Simple + + setAdvancedMode(true)} + className={cn( + 'group m-1 flex items-center justify-center gap-2 rounded-md px-4 py-2 text-sm text-black transition-all duration-200 ease-in-out radix-state-active:bg-white radix-state-active:text-black dark:text-white dark:radix-state-active:bg-gray-600', + isSmallScreen + ? 'flex-row items-center justify-center text-sm text-gray-700 radix-state-active:bg-gray-100 radix-state-active:text-black dark:text-gray-300 dark:radix-state-active:text-white' + : 'bg-white radix-state-active:bg-gray-100 dark:bg-gray-700', + 'w-full', + )} + value="advanced" + style={{ userSelect: 'none' }} + > + + Advanced + + +
- -
-
- -
-
- -
-
- -
-
-
- -
-
- -
-
- -
+ +
+
+ +
+
+
- +
+ +
+
+
+ +
+
+ +
+
+ +
+
+ - -
-
- -
-
-
- -
-
- -
-
- -
+ +
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+ +
+ {autoTranscribeAudio && (
- -
- {autoTranscribeAudio && ( -
- -
- )} -
- -
-
-
- -
-
- -
-
- -
-
- +
- {engineTTS === 'browser' && ( -
- -
- )} + )} +
+ +
+
+
+ +
+
+ +
+
+ +
+
+ +
+ {engineTTS === 'browser' && (
- -
-
- +
+ )} +
+
- - - +
+ +
+
+ + ); } diff --git a/client/src/components/ui/Dropdown.tsx b/client/src/components/ui/Dropdown.tsx index f51763cd63a..bd967808315 100644 --- a/client/src/components/ui/Dropdown.tsx +++ b/client/src/components/ui/Dropdown.tsx @@ -46,7 +46,7 @@ const Dropdown: FC = ({ = ({ diff --git a/client/src/components/ui/OriginalDialog.tsx b/client/src/components/ui/OriginalDialog.tsx index ef0508fbe9f..c09ac786f8f 100644 --- a/client/src/components/ui/OriginalDialog.tsx +++ b/client/src/components/ui/OriginalDialog.tsx @@ -48,7 +48,7 @@ const DialogContent = React.forwardRef< > {children} {showCloseButton && ( - + Close diff --git a/client/src/components/ui/Switch.tsx b/client/src/components/ui/Switch.tsx index b706cb50140..21b1081dac7 100644 --- a/client/src/components/ui/Switch.tsx +++ b/client/src/components/ui/Switch.tsx @@ -10,6 +10,7 @@ const Switch = React.forwardRef<