🎨 feat: enhance Chat Input UI, File Mgmt. UI, Bookmarks a11y (#5112)

* 🎨 feat: improve file display and overflow handling in SidePanel components

* 🎨 feat: enhance bookmarks management UI and improve accessibility features

* 🎨 feat: enhance BookmarkTable and BookmarkTableRow components for improved layout and performance

* 🎨 feat: enhance file display and interaction in FilesView and ImagePreview components

* 🎨 feat: adjust minimum width for filename filter input in DataTable component

* 🎨 feat: enhance file upload UI with improved layout and styling adjustments

* 🎨 feat: add surface-hover-alt color and update FileContainer styling for improved UI

* 🎨 feat: update ImagePreview component styling for improved visual consistency

* 🎨 feat: add MaximizeChatSpace component and integrate chat space maximization feature

* 🎨 feat: enhance DataTable component with transition effects and update Checkbox styling for improved accessibility

* fix: enhance a11y for Bookmark buttons by adding space key support, ARIA labels, and correct html role for key presses

* fix: return focus back to trigger for BookmarkEditDialog (Edit and new bookmark buttons)

* refactor: ShareButton and ExportModal components children prop support; refactor DropdownPopup item handling

* refactor: enhance ExportAndShareMenu and ShareButton components with improved props handling and accessibility features

* refactor: add ref prop support to MenuItemProps and update ExportAndShareMenu and DropdownPopup components so focus correctly returns to menu item

* refactor: enhance ConvoOptions and DeleteButton components with improved props handling and accessibility features

* refactor: add triggerRef support to DeleteButton and update ConvoOptions for improved dialog handling

* refactor: accessible bookmarks menu

* refactor: improve styling and accessibility for bookmarks components

* refactor: add focusLoop support to DropdownPopup and update BookmarkMenu with Tooltip

* refactor: integrate TooltipAnchor into ExportAndShareMenu for enhanced accessibility

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
Marco Beretta 2024-12-29 23:31:41 +01:00 committed by GitHub
parent d9c59b08e6
commit cb1921626e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 767 additions and 484 deletions

View file

@ -1,22 +1,26 @@
import { useState, type FC, useCallback } from 'react';
import { useState, useId, useCallback, useMemo, useRef } from 'react';
import { useRecoilValue } from 'recoil';
import * as Ariakit from '@ariakit/react';
import { BookmarkPlusIcon } from 'lucide-react';
import { useQueryClient } from '@tanstack/react-query';
import { Constants, QueryKeys } from 'librechat-data-provider';
import { Menu, MenuButton, MenuItems } from '@headlessui/react';
import { BookmarkFilledIcon, BookmarkIcon } from '@radix-ui/react-icons';
import type { TConversationTag } from 'librechat-data-provider';
import type { FC } from 'react';
import type * as t from '~/common';
import { useConversationTagsQuery, useTagConversationMutation } from '~/data-provider';
import { BookmarkMenuItems } from './Bookmarks/BookmarkMenuItems';
import { DropdownPopup, TooltipAnchor } from '~/components/ui';
import { BookmarkContext } from '~/Providers/BookmarkContext';
import { BookmarkEditDialog } from '~/components/Bookmarks';
import { useBookmarkSuccess, useLocalize } from '~/hooks';
import { NotificationSeverity } from '~/common';
import { useToastContext } from '~/Providers';
import { useBookmarkSuccess } from '~/hooks';
import { Spinner } from '~/components';
import { cn, logger } from '~/utils';
import store from '~/store';
const BookmarkMenu: FC = () => {
const localize = useLocalize();
const queryClient = useQueryClient();
const { showToast } = useToastContext();
@ -24,13 +28,20 @@ const BookmarkMenu: FC = () => {
const conversationId = conversation?.conversationId ?? '';
const updateConvoTags = useBookmarkSuccess(conversationId);
const [open, setOpen] = useState(false);
const menuId = useId();
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [tags, setTags] = useState<string[]>(conversation?.tags || []);
const mutation = useTagConversationMutation(conversationId, {
onSuccess: (newTags: string[]) => {
onSuccess: (newTags: string[], vars) => {
setTags(newTags);
updateConvoTags(newTags);
const tagElement = document.getElementById(vars.tag);
console.log('tagElement', tagElement);
if (tagElement) {
setTimeout(() => tagElement.focus(), 2);
}
},
onError: () => {
showToast({
@ -38,6 +49,13 @@ const BookmarkMenu: FC = () => {
severity: NotificationSeverity.ERROR,
});
},
onMutate: (vars) => {
const tagElement = document.getElementById(vars.tag);
console.log('tagElement', tagElement);
if (tagElement) {
setTimeout(() => tagElement.focus(), 2);
}
},
});
const { data } = useConversationTagsQuery();
@ -60,22 +78,62 @@ const BookmarkMenu: FC = () => {
}
logger.log('tag_mutation', 'BookmarkMenu - handleSubmit: tags before setting', tags);
const allTags =
queryClient.getQueryData<TConversationTag[]>([QueryKeys.conversationTags]) ?? [];
const existingTags = allTags.map((t) => t.tag);
const filteredTags = tags.filter((t) => existingTags.includes(t));
logger.log('tag_mutation', 'BookmarkMenu - handleSubmit: tags after filtering', filteredTags);
const newTags = filteredTags.includes(tag)
? filteredTags.filter((t) => t !== tag)
: [...filteredTags, tag];
logger.log('tag_mutation', 'BookmarkMenu - handleSubmit: tags after', newTags);
mutation.mutate({
tags: newTags,
tag,
});
},
[tags, conversationId, mutation, queryClient, showToast],
);
const newBookmarkRef = useRef<HTMLButtonElement>(null);
const dropdownItems: t.MenuItemProps[] = useMemo(() => {
const items: t.MenuItemProps[] = [
{
id: '%___new___bookmark___%',
label: localize('com_ui_bookmarks_new'),
icon: <BookmarkPlusIcon className="size-4" />,
hideOnClick: false,
ref: newBookmarkRef,
render: (props) => <button {...props} />,
onClick: () => setIsDialogOpen(true),
},
];
if (data) {
for (const tag of data) {
const isSelected = tags.includes(tag.tag);
items.push({
id: tag.tag,
hideOnClick: false,
label: tag.tag,
icon: isSelected ? (
<BookmarkFilledIcon className="size-4" />
) : (
<BookmarkIcon className="size-4" />
),
onClick: () => handleSubmit(tag.tag),
disabled: mutation.isLoading,
});
}
}
return items;
}, [tags, data, handleSubmit, mutation.isLoading, localize]);
if (!isActiveConvo) {
return null;
}
@ -90,47 +148,43 @@ const BookmarkMenu: FC = () => {
return <BookmarkIcon className="icon-sm" aria-label="Bookmark" />;
};
const handleToggleOpen = () => setOpen(!open);
return (
<>
<Menu as="div" className="group relative">
{({ open }) => (
<>
<MenuButton
aria-label="Add bookmarks"
className={cn(
'mt-text-sm flex size-10 items-center justify-center gap-2 rounded-lg border border-border-light text-sm transition-colors duration-200 hover:bg-surface-hover',
open ? 'bg-surface-hover' : '',
)}
data-testid="bookmark-menu"
>
{renderButtonContent()}
</MenuButton>
<MenuItems
anchor="bottom start"
className="overflow-hidden rounded-lg bg-header-primary p-1.5 shadow-lg outline-none"
>
<BookmarkContext.Provider value={{ bookmarks: data || [] }}>
<BookmarkMenuItems
handleToggleOpen={handleToggleOpen}
tags={tags}
handleSubmit={handleSubmit}
/>
</BookmarkContext.Provider>
</MenuItems>
</>
)}
</Menu>
<BookmarkContext.Provider value={{ bookmarks: data || [] }}>
<DropdownPopup
focusLoop={true}
menuId={menuId}
isOpen={isMenuOpen}
setIsOpen={setIsMenuOpen}
trigger={
<TooltipAnchor
description={localize('com_ui_bookmarks_add')}
render={
<Ariakit.MenuButton
id="bookmark-menu-button"
aria-label={localize('com_ui_bookmarks_add')}
className={cn(
'mt-text-sm flex size-10 items-center justify-center gap-2 rounded-lg border border-border-light text-sm transition-colors duration-200 hover:bg-surface-hover',
isMenuOpen ? 'bg-surface-hover' : '',
)}
data-testid="bookmark-menu"
>
{renderButtonContent()}
</Ariakit.MenuButton>
}
/>
}
items={dropdownItems}
/>
<BookmarkEditDialog
context="BookmarkMenu - BookmarkEditDialog"
conversation={conversation}
tags={tags}
setTags={setTags}
open={open}
setOpen={setOpen}
open={isDialogOpen}
setOpen={setIsDialogOpen}
triggerRef={newBookmarkRef}
conversationId={conversationId}
context="BookmarkMenu - BookmarkEditDialog"
/>
</>
</BookmarkContext.Provider>
);
};