⌨️ 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>
This commit is contained in:
Marco Beretta 2024-09-10 10:11:39 -09:00 committed by GitHub
parent 1a1e6850a3
commit d6c0121b19
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 507 additions and 513 deletions

View file

@ -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 (
<Tabs.Content
value={SettingsTabValues.DATA}
role="tabpanel"
className="w-full md:min-h-[271px]"
ref={dataTabRef}
>
<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">
<ImportConversations />
</div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
<SharedLinks />
</div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
<RevokeKeysButton all={true} />
</div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
<DeleteCacheButton />
</div>
<div className="border-b pb-3 last-of-type:border-b-0 dark:border-gray-600">
<ClearChatsButton
confirmClear={confirmClearConvos}
onClick={clearConvos}
showText={true}
mutation={clearConvosMutation}
/>
</div>
<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">
<ImportConversations />
</div>
</Tabs.Content>
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
<SharedLinks />
</div>
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
<RevokeKeysButton all={true} />
</div>
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
<DeleteCacheButton />
</div>
<div className="border-b border-border-medium pb-3 last-of-type:border-b-0">
<ClearChatsButton
confirmClear={confirmClearConvos}
onClick={clearConvos}
showText={true}
mutation={clearConvosMutation}
/>
</div>
</div>
);
}

View file

@ -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<HTMLInputElement>(null);
const { showToast } = useToastContext();
const [, setErrors] = useState<string[]>([]);
@ -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<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
handleFiles(file);
}
};
const handleImportClick = () => {
fileInputRef.current?.click();
};
const handleKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
handleImportClick();
}
};
return (
<div className="flex items-center justify-between">
<div>{localize('com_ui_import_conversation_info')}</div>
<label htmlFor={'import-conversations-file'} className="btn btn-neutral relative">
<button
onClick={handleImportClick}
onKeyDown={handleKeyDown}
disabled={!allowImport}
aria-label={localize('com_ui_import_conversation')}
className="btn btn-neutral relative"
>
{allowImport ? (
<Import className="mr-1 flex h-4 w-4 items-center stroke-1" />
) : (
<Spinner className="mr-1 w-4" />
)}
<span>{localize('com_ui_import_conversation')}</span>
<input
id={'import-conversations-file'}
disabled={!allowImport}
value=""
type="file"
className={cn('hidden')}
accept=".json"
onChange={handleFileChange}
/>
</label>
</button>
<input
ref={fileInputRef}
type="file"
className={cn('hidden')}
accept=".json"
onChange={handleFileChange}
aria-hidden="true"
/>
</div>
);
}