🤲 a11y: Keyboard Accessibility for Account Settings (#3666)

* refactor(NavLinks -> AccountSettings): add theming, fix minor type issues, and rename component

* fix: closes #3499 - Account settings menu is not keyboard accessible

* fix(DataTable): type issue causing application crash, update contextMap key for storage
This commit is contained in:
Danny Avila 2024-08-16 15:09:03 -04:00 committed by GitHub
parent 6ad65ff065
commit 3826af5909
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 85 additions and 70 deletions

View file

@ -112,10 +112,12 @@ export interface NavProps {
defaultActive?: string;
}
interface ColumnMeta {
meta: {
size: number | string;
};
export interface DataColumnMeta {
meta:
| {
size: number | string;
}
| undefined;
}
export enum Panel {
@ -157,7 +159,7 @@ export type AssistantPanelProps = {
setActivePanel: React.Dispatch<React.SetStateAction<Panel>>;
};
export type AugmentedColumnDef<TData, TValue> = ColumnDef<TData, TValue> & ColumnMeta;
export type AugmentedColumnDef<TData, TValue> = ColumnDef<TData, TValue> & DataColumnMeta;
export type TSetOption = SetOption;

View file

@ -43,7 +43,7 @@ interface DataTableProps<TData, TValue> {
const contextMap = {
[FileContext.filename]: 'com_ui_name',
[FileContext.updatedAt]: 'com_ui_date',
[FileContext.source]: 'com_ui_storage',
[FileContext.filterSource]: 'com_ui_storage',
[FileContext.context]: 'com_ui_context',
[FileContext.bytes]: 'com_ui_size',
};
@ -108,13 +108,13 @@ export default function DataTable<TData, TValue>({ columns, data }: DataTablePro
</Button>
<Input
placeholder={localize('com_files_filter')}
value={(table.getColumn('filename')?.getFilterValue() as string) ?? ''}
value={(table.getColumn('filename')?.getFilterValue() as string | undefined) ?? ''}
onChange={(event) => table.getColumn('filename')?.setFilterValue(event.target.value)}
className="max-w-sm border-border-light placeholder:text-text-secondary"
className="max-w-sm border-border-medium placeholder:text-text-secondary"
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ml-auto">
<Button variant="outline" className="ml-auto border border-border-medium">
<ListFilter className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
@ -132,7 +132,7 @@ export default function DataTable<TData, TValue>({ columns, data }: DataTablePro
key={column.id}
className="cursor-pointer capitalize dark:text-white dark:hover:bg-gray-800"
checked={column.getIsVisible()}
onCheckedChange={(value) => column.toggleVisibility(!!value)}
onCheckedChange={(value) => column.toggleVisibility(Boolean(value))}
>
{localize(contextMap[column.id])}
</DropdownMenuCheckboxItem>
@ -184,7 +184,7 @@ export default function DataTable<TData, TValue>({ columns, data }: DataTablePro
>
{row.getVisibleCells().map((cell, index) => {
const maxWidth =
(cell.column.columnDef as AugmentedColumnDef<TData, TValue>).meta.size ??
(cell.column.columnDef as AugmentedColumnDef<TData, TValue>).meta?.size ??
'auto';
const style: Style = {};
@ -225,7 +225,7 @@ export default function DataTable<TData, TValue>({ columns, data }: DataTablePro
)}
</div>
<Button
className="select-none dark:border-gray-500 dark:hover:bg-gray-600"
className="select-none border-border-medium"
variant="outline"
size="sm"
onClick={() => table.previousPage()}
@ -234,7 +234,7 @@ export default function DataTable<TData, TValue>({ columns, data }: DataTablePro
{localize('com_ui_prev')}
</Button>
<Button
className="select-none dark:border-gray-500 dark:hover:bg-gray-600"
className="select-none border-border-medium"
variant="outline"
size="sm"
onClick={() => table.nextPage()}

View file

@ -45,7 +45,7 @@ interface DataTableProps<TData, TValue> {
const contextMap = {
[FileContext.filename]: 'com_ui_name',
[FileContext.updatedAt]: 'com_ui_date',
[FileContext.source]: 'com_ui_storage',
[FileContext.filterSource]: 'com_ui_storage',
[FileContext.context]: 'com_ui_context',
[FileContext.bytes]: 'com_ui_size',
};
@ -121,7 +121,7 @@ export default function DataTableFile<TData, TValue>({
{' '}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="ml-auto">
<Button variant="outline" className="ml-auto border border-border-medium">
<ListFilter className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
@ -138,7 +138,7 @@ export default function DataTableFile<TData, TValue>({
key={column.id}
className="cursor-pointer capitalize dark:text-white dark:hover:bg-gray-800"
checked={column.getIsVisible()}
onCheckedChange={(value) => column.toggleVisibility(!!value)}
onCheckedChange={(value) => column.toggleVisibility(Boolean(value))}
>
{localize(contextMap[column.id])}
</DropdownMenuCheckboxItem>
@ -148,15 +148,11 @@ export default function DataTableFile<TData, TValue>({
</DropdownMenu>
<Input
placeholder={localize('com_files_filter')}
value={(table.getColumn('filename')?.getFilterValue() as string) ?? ''}
value={(table.getColumn('filename')?.getFilterValue() as string | undefined) ?? ''}
onChange={(event) => table.getColumn('filename')?.setFilterValue(event.target.value)}
className="max-w-sm border-border-light placeholder:text-text-secondary"
/>
<UploadFileButton
onClick={() => {
console.log('click');
}}
className="max-w-sm border-border-medium placeholder:text-text-secondary"
/>
<UploadFileButton onClick={() => console.log('click')} />
</div>
</div>
</div>
@ -213,7 +209,7 @@ export default function DataTableFile<TData, TValue>({
>
{row.getVisibleCells().map((cell, index) => {
const maxWidth =
(cell.column.columnDef as AugmentedColumnDef<TData, TValue>).meta.size ??
(cell.column.columnDef as AugmentedColumnDef<TData, TValue>).meta?.size ??
'auto';
const style: Style = {};

View file

@ -15,7 +15,7 @@ import Logout from './Logout';
import { cn } from '~/utils/';
import store from '~/store';
function NavLinks() {
function AccountSettings() {
const localize = useLocalize();
const { user, isAuthenticated } = useAuthContext();
const { data: startupConfig } = useGetStartupConfig();
@ -26,6 +26,7 @@ function NavLinks() {
const [showFiles, setShowFiles] = useRecoilState(store.showFiles);
const avatarSrc = useAvatar(user);
const name = user?.avatar ?? user?.username ?? '';
return (
<>
@ -33,15 +34,16 @@ function NavLinks() {
{({ open }) => (
<>
<MenuButton
aria-label={localize('com_nav_account_settings')}
className={cn(
'group-ui-open:bg-gray-100 dark:group-ui-open:bg-gray-700 duration-350 mt-text-sm flex h-auto w-full items-center gap-2 rounded-lg p-2 text-sm transition-colors hover:bg-gray-100 dark:hover:bg-gray-800',
open ? 'bg-gray-100 dark:bg-gray-800' : '',
'group-ui-open:bg-surface-tertiary duration-350 mt-text-sm flex h-auto w-full items-center gap-2 rounded-lg p-2 text-sm transition-colors hover:bg-surface-secondary',
open ? 'bg-surface-secondary' : '',
)}
data-testid="nav-user"
>
<div className="-ml-0.9 -mt-0.8 h-8 w-8 flex-shrink-0">
<div className="relative flex">
{!user?.avatar && !user?.username ? (
{name.length === 0 ? (
<div
style={{
backgroundColor: 'rgb(121, 137, 255)',
@ -49,20 +51,20 @@ function NavLinks() {
height: '32px',
boxShadow: 'rgba(240, 246, 252, 0.1) 0px 0px 0px 1px',
}}
className="relative flex items-center justify-center rounded-full p-1 text-white"
className="relative flex items-center justify-center rounded-full p-1 text-text-primary"
>
<UserIcon />
</div>
) : (
<img className="rounded-full" src={user.avatar || avatarSrc} alt="avatar" />
<img className="rounded-full" src={user?.avatar ?? avatarSrc} alt="avatar" />
)}
</div>
</div>
<div
className="mt-2 grow overflow-hidden text-ellipsis whitespace-nowrap text-left text-black dark:text-gray-100"
className="mt-2 grow overflow-hidden text-ellipsis whitespace-nowrap text-left text-text-primary"
style={{ marginTop: '0', marginLeft: '0' }}
>
{user?.name || user?.username || localize('com_nav_user')}
{user?.name ?? user?.username ?? localize('com_nav_user')}
</div>
</MenuButton>
@ -75,47 +77,56 @@ function NavLinks() {
leaveFrom="translate-y-0 opacity-100"
leaveTo="translate-y-2 opacity-0"
>
<MenuItems className="absolute bottom-full left-0 z-[100] mb-1 mt-1 w-full translate-y-0 overflow-hidden rounded-lg border border-gray-300 bg-white p-1.5 opacity-100 shadow-lg outline-none dark:border-gray-600 dark:bg-gray-700">
<MenuItems className="absolute bottom-full left-0 z-[100] mb-1 mt-1 w-full translate-y-0 overflow-hidden rounded-lg border border-border-medium bg-header-primary p-1.5 opacity-100 shadow-lg outline-none">
<div className="text-token-text-secondary ml-3 mr-2 py-2 text-sm" role="none">
{user?.email || localize('com_nav_user')}
{user?.email ?? localize('com_nav_user')}
</div>
<div className="my-1.5 h-px bg-black/10 dark:bg-white/10" role="none" />
{startupConfig?.checkBalance &&
balanceQuery.data &&
<div className="my-1.5 h-px border-b border-border-medium" role="none" />
{startupConfig?.checkBalance === true &&
balanceQuery.data != null &&
!isNaN(parseFloat(balanceQuery.data)) && (
<>
<div className="text-token-text-secondary ml-3 mr-2 py-2 text-sm">
{`Balance: ${parseFloat(balanceQuery.data).toFixed(2)}`}
</div>
<div className="my-1.5 h-px bg-black/10 dark:bg-white/10" role="none" />
<div className="my-1.5 h-px border-b border-border-medium" role="none" />
</>
)}
<MenuItem as="div">
<NavLink
svg={() => <FileText className="icon-md" />}
text={localize('com_nav_my_files')}
clickHandler={() => setShowFiles(true)}
/>
<MenuItem>
{({ focus }) => (
<NavLink
className={focus ? 'bg-surface-hover' : ''}
svg={() => <FileText className="icon-md" />}
text={localize('com_nav_my_files')}
clickHandler={() => setShowFiles(true)}
/>
)}
</MenuItem>
{startupConfig?.helpAndFaqURL !== '/' && (
<MenuItem as="div">
<NavLink
svg={() => <LinkIcon />}
text={localize('com_nav_help_faq')}
clickHandler={() => window.open(startupConfig?.helpAndFaqURL, '_blank')}
/>
<MenuItem>
{({ focus }) => (
<NavLink
className={focus ? 'bg-surface-hover' : ''}
svg={() => <LinkIcon />}
text={localize('com_nav_help_faq')}
clickHandler={() => window.open(startupConfig?.helpAndFaqURL, '_blank')}
/>
)}
</MenuItem>
)}
<MenuItem as="div">
<NavLink
svg={() => <GearIcon className="icon-md" />}
text={localize('com_nav_settings')}
clickHandler={() => setShowSettings(true)}
/>
<MenuItem>
{({ focus }) => (
<NavLink
className={focus ? 'bg-surface-hover' : ''}
svg={() => <GearIcon className="icon-md" />}
text={localize('com_nav_settings')}
clickHandler={() => setShowSettings(true)}
/>
)}
</MenuItem>
<div className="my-1.5 h-px bg-black/10 dark:bg-white/10" role="none" />
<MenuItem as="div">
<Logout />
<div className="my-1.5 h-px border-b border-border-medium" role="none" />
<MenuItem>
{({ focus }) => <Logout className={focus ? 'bg-surface-hover' : ''} />}
</MenuItem>
</MenuItems>
</Transition>
@ -128,4 +139,4 @@ function NavLinks() {
);
}
export default memo(NavLinks);
export default memo(AccountSettings);

View file

@ -1,15 +1,20 @@
import { forwardRef } from 'react';
import { LogOutIcon } from '../svg';
import { useAuthContext } from '~/hooks/AuthContext';
import { LogOutIcon } from '~/components/svg';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
const Logout = forwardRef(() => {
const Logout = forwardRef<HTMLButtonElement, { className?: string }>((props, ref) => {
const { logout } = useAuthContext();
const localize = useLocalize();
return (
<button
className="group group flex w-full cursor-pointer items-center gap-2 rounded p-2.5 text-sm transition-colors duration-200 hover:bg-gray-500/10 focus:ring-0 dark:text-white dark:hover:bg-gray-600"
ref={ref}
className={cn(
'group group flex w-full cursor-pointer items-center gap-2 rounded p-2.5 text-sm text-text-primary transition-colors duration-200 hover:bg-surface-hover data-[focus]:bg-surface-hover',
props.className ?? '',
)}
onClick={() => logout()}
>
<LogOutIcon />

View file

@ -15,11 +15,11 @@ import { useConversationsInfiniteQuery } from '~/data-provider';
import { TooltipProvider, Tooltip } from '~/components/ui';
import { Conversations } from '~/components/Conversations';
import BookmarkNav from './Bookmarks/BookmarkNav';
import AccountSettings from './AccountSettings';
import { useSearchContext } from '~/Providers';
import { Spinner } from '~/components/svg';
import SearchBar from './SearchBar';
import NavToggle from './NavToggle';
import NavLinks from './NavLinks';
import NewChat from './NewChat';
import { cn } from '~/utils';
import store from '~/store';
@ -196,7 +196,7 @@ const Nav = ({
<Spinner className={cn('m-1 mx-auto mb-4 h-4 w-4 text-text-primary')} />
)}
</div>
<NavLinks />
<AccountSettings />
</nav>
</div>
</div>

View file

@ -9,14 +9,14 @@ interface Props {
disabled?: boolean;
}
const NavLink: FC<Props> = forwardRef<HTMLAnchorElement, Props>((props, ref) => {
const NavLink: FC<Props> = forwardRef<HTMLButtonElement, Props>((props, ref) => {
const { svg, text, clickHandler, disabled, className = '' } = props;
const defaultProps: {
className: string;
onClick?: () => void;
} = {
className: cn(
'flex gap-2 rounded p-2.5 text-sm cursor-pointer focus:ring-0 group items-center transition-colors duration-200 hover:bg-gray-500/10 dark:text-white dark:hover:bg-gray-600',
'w-full flex gap-2 rounded p-2.5 text-sm cursor-pointer group items-center transition-colors duration-200 text-text-primary hover:bg-surface-hover',
className,
{
'opacity-50 pointer-events-none': disabled,
@ -29,10 +29,10 @@ const NavLink: FC<Props> = forwardRef<HTMLAnchorElement, Props>((props, ref) =>
}
return (
<a {...defaultProps} ref={ref}>
<button {...defaultProps} ref={ref}>
{svg()}
{text}
</a>
</button>
);
});

View file

@ -5,7 +5,6 @@ export { default as Logout } from './Logout';
export { default as MobileNav } from './MobileNav';
export { default as Nav } from './Nav';
export { default as NavLink } from './NavLink';
export { default as NavLinks } from './NavLinks';
export { default as NewChat } from './NewChat';
export { default as SearchBar } from './SearchBar';
export { default as Settings } from './Settings';

View file

@ -556,6 +556,7 @@ export default {
com_endpoint_config_key_google_service_account: 'Create a Service Account',
com_endpoint_config_key_google_vertex_api_role:
'Make sure to click \'Create and Continue\' to give at least the \'Vertex AI User\' role. Lastly, create a JSON key to import here.',
com_nav_account_settings: 'Account Settings',
com_nav_font_size: 'Message Font Size',
com_nav_font_size_xs: 'Extra Small',
com_nav_font_size_sm: 'Small',

View file

@ -22,6 +22,7 @@ export enum FileContext {
filename = 'filename',
updatedAt = 'updatedAt',
source = 'source',
filterSource = 'filterSource',
context = 'context',
bytes = 'bytes',
}