mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-07 02:58:50 +01:00
⌨️ refactor: Favorite Item Selection & Keyboard Navigation/Focus Improvements (#10952)
* refactor: Reuse conversation switching logic from useSelectMention hook for Favorite Items - Added onSelectEndpoint prop to FavoriteItem for improved endpoint selection handling. - Refactored conversation initiation logic to utilize the new prop instead of direct navigation. - Updated FavoritesList to pass onSelectEndpoint to FavoriteItem, streamlining the interaction flow. - Replaced EndpointIcon with MinimalIcon for a cleaner UI representation of favorite models. * refactor: Enhance FavoriteItem and FavoritesList for improved accessibility and interaction - Added onRemoveFocus prop to FavoriteItem for better focus management after item removal. - Refactored event handling in FavoriteItem to support keyboard interactions for accessibility. - Updated FavoritesList to utilize the new onRemoveFocus prop, ensuring focus shifts appropriately after removing favorites. - Enhanced aria-labels and roles for better screen reader support and user experience. * refactor: Enhance EndpointModelItem for improved accessibility and interaction - Added useRef and useState hooks to manage active state and focus behavior. - Implemented MutationObserver to track changes in the data-active-item attribute for better accessibility. - Refactored favorite button handling to improve interaction and accessibility. - Updated button tabIndex based on active state to enhance keyboard navigation. * chore: Update Radix UI dependencies in package-lock and package.json files - Upgraded @radix-ui/react-alert-dialog and @radix-ui/react-dialog to version 1.1.15 across client and packages/client. - Updated related dependencies for improved compatibility and performance. - Removed outdated debug module references from package-lock.json. * refactor: Improve accessibility and interaction in conversation options - Added event handling to prevent unintended actions when renaming conversations. - Updated ConvoOptions to use Ariakit components for better accessibility and interaction. - Refactored button handlers for sharing and deleting conversations for clarity and consistency. - Enhanced dialog components with proper aria attributes and improved structure for better screen reader support. * refactor: Improve nested dialog accessibility for deleting shared link - Eliminated the setShareDialogOpen prop from both ShareButton and SharedLinkButton components to streamline the code. - Updated the delete mutation success handler in SharedLinkButton to improve focus management for accessibility. - Enhanced the OGDialog component in SharedLinkButton with a triggerRef for better interaction.
This commit is contained in:
parent
5b0cce2e2a
commit
4d7e6b4a58
12 changed files with 670 additions and 219 deletions
|
|
@ -155,6 +155,9 @@ export default function Conversation({ conversation, retainView, toggleNav }: Co
|
|||
if (renaming) {
|
||||
return;
|
||||
}
|
||||
if (e.target !== e.currentTarget) {
|
||||
return;
|
||||
}
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
handleNavigation(false);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useId, useRef, memo, useCallback, useMemo } from 'react';
|
||||
import * as Menu from '@ariakit/react/menu';
|
||||
import * as Ariakit from '@ariakit/react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { QueryKeys } from 'librechat-data-provider';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
|
|
@ -49,6 +49,7 @@ function ConvoOptions({
|
|||
const { conversationId: currentConvoId } = useParams();
|
||||
const { newConversation } = useNewConvo();
|
||||
|
||||
const menuId = useId();
|
||||
const shareButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const deleteButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const [showShareDialog, setShowShareDialog] = useState(false);
|
||||
|
|
@ -106,11 +107,11 @@ function ConvoOptions({
|
|||
const isArchiveLoading = archiveConvoMutation.isLoading;
|
||||
const isDeleteLoading = deleteMutation.isLoading;
|
||||
|
||||
const handleShareClick = useCallback(() => {
|
||||
const shareHandler = useCallback(() => {
|
||||
setShowShareDialog(true);
|
||||
}, []);
|
||||
|
||||
const handleDeleteClick = useCallback(() => {
|
||||
const deleteHandler = useCallback(() => {
|
||||
setShowDeleteDialog(true);
|
||||
}, []);
|
||||
|
||||
|
|
@ -189,13 +190,15 @@ function ConvoOptions({
|
|||
() => [
|
||||
{
|
||||
label: localize('com_ui_share'),
|
||||
onClick: handleShareClick,
|
||||
onClick: shareHandler,
|
||||
icon: <Share2 className="icon-sm mr-2 text-text-primary" aria-hidden="true" />,
|
||||
show: startupConfig && startupConfig.sharedLinksEnabled,
|
||||
hideOnClick: false,
|
||||
ref: shareButtonRef,
|
||||
ariaHasPopup: 'dialog' as const,
|
||||
ariaControls: 'share-conversation-dialog',
|
||||
/** NOTE: THE FOLLOWING PROPS ARE REQUIRED FOR MENU ITEMS THAT OPEN DIALOGS */
|
||||
hideOnClick: false,
|
||||
ref: shareButtonRef,
|
||||
render: (props) => <button {...props} />,
|
||||
},
|
||||
{
|
||||
label: localize('com_ui_rename'),
|
||||
|
|
@ -224,29 +227,29 @@ function ConvoOptions({
|
|||
},
|
||||
{
|
||||
label: localize('com_ui_delete'),
|
||||
onClick: handleDeleteClick,
|
||||
onClick: deleteHandler,
|
||||
icon: <Trash className="icon-sm mr-2 text-text-primary" aria-hidden="true" />,
|
||||
hideOnClick: false,
|
||||
ref: deleteButtonRef,
|
||||
ariaHasPopup: 'dialog' as const,
|
||||
ariaControls: 'delete-conversation-dialog',
|
||||
/** NOTE: THE FOLLOWING PROPS ARE REQUIRED FOR MENU ITEMS THAT OPEN DIALOGS */
|
||||
hideOnClick: false,
|
||||
ref: deleteButtonRef,
|
||||
render: (props) => <button {...props} />,
|
||||
},
|
||||
],
|
||||
[
|
||||
localize,
|
||||
handleShareClick,
|
||||
shareHandler,
|
||||
startupConfig,
|
||||
renameHandler,
|
||||
handleDuplicateClick,
|
||||
deleteHandler,
|
||||
isArchiveLoading,
|
||||
isDuplicateLoading,
|
||||
handleArchiveClick,
|
||||
isArchiveLoading,
|
||||
handleDeleteClick,
|
||||
handleDuplicateClick,
|
||||
],
|
||||
);
|
||||
|
||||
const menuId = useId();
|
||||
|
||||
const buttonClassName = cn(
|
||||
'inline-flex h-7 w-7 items-center justify-center 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:opacity-50',
|
||||
isActiveConvo === true || isPopoverActive
|
||||
|
|
@ -292,13 +295,13 @@ function ConvoOptions({
|
|||
</span>
|
||||
<DropdownPopup
|
||||
portal={true}
|
||||
mountByState={true}
|
||||
menuId={menuId}
|
||||
focusLoop={true}
|
||||
unmountOnHide={true}
|
||||
preserveTabOrder={true}
|
||||
isOpen={isPopoverActive}
|
||||
setIsOpen={setIsPopoverActive}
|
||||
trigger={
|
||||
<Menu.MenuButton
|
||||
<Ariakit.MenuButton
|
||||
id={`conversation-menu-${conversationId}`}
|
||||
aria-label={localize('com_nav_convo_menu_options')}
|
||||
aria-readonly={undefined}
|
||||
|
|
@ -318,10 +321,9 @@ function ConvoOptions({
|
|||
}}
|
||||
>
|
||||
<Ellipsis className="icon-md text-text-secondary" aria-hidden={true} />
|
||||
</Menu.MenuButton>
|
||||
</Ariakit.MenuButton>
|
||||
}
|
||||
items={dropdownItems}
|
||||
menuId={menuId}
|
||||
className="z-30"
|
||||
/>
|
||||
{showShareDialog && (
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
Button,
|
||||
Spinner,
|
||||
OGDialog,
|
||||
OGDialogClose,
|
||||
OGDialogTitle,
|
||||
OGDialogHeader,
|
||||
OGDialogContent,
|
||||
|
|
@ -81,14 +82,14 @@ export function DeleteConversationDialog({
|
|||
|
||||
return (
|
||||
<OGDialogContent
|
||||
title={localize('com_ui_delete_confirm', { title })}
|
||||
className="w-11/12 max-w-md"
|
||||
showCloseButton={false}
|
||||
aria-describedby="delete-conversation-description"
|
||||
>
|
||||
<OGDialogHeader>
|
||||
<OGDialogTitle>{localize('com_ui_delete_conversation')}</OGDialogTitle>
|
||||
</OGDialogHeader>
|
||||
<div id="delete-conversation-dialog" className="w-full truncate">
|
||||
<div id="delete-conversation-description" className="w-full truncate">
|
||||
<Trans
|
||||
i18nKey="com_ui_delete_confirm_strong"
|
||||
values={{ title }}
|
||||
|
|
@ -96,9 +97,11 @@ export function DeleteConversationDialog({
|
|||
/>
|
||||
</div>
|
||||
<div className="flex justify-end gap-4 pt-4">
|
||||
<Button aria-label="cancel" variant="outline" onClick={() => setShowDeleteDialog(false)}>
|
||||
{localize('com_ui_cancel')}
|
||||
</Button>
|
||||
<OGDialogClose asChild>
|
||||
<Button aria-label="cancel" variant="outline">
|
||||
{localize('com_ui_cancel')}
|
||||
</Button>
|
||||
</OGDialogClose>
|
||||
<Button variant="destructive" onClick={confirmDelete} disabled={deleteMutation.isLoading}>
|
||||
{deleteMutation.isLoading ? <Spinner /> : localize('com_ui_delete')}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ export default function ShareButton({
|
|||
share={share}
|
||||
conversationId={conversationId}
|
||||
targetMessageId={latestMessage?.messageId}
|
||||
setShareDialogOpen={onOpenChange}
|
||||
showQR={showQR}
|
||||
setShowQR={setShowQR}
|
||||
setSharedLink={setSharedLink}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
import { useState, useCallback } from 'react';
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
import { Trans } from 'react-i18next';
|
||||
import { QrCode, RotateCw, Trash2 } from 'lucide-react';
|
||||
import {
|
||||
Button,
|
||||
OGDialog,
|
||||
Spinner,
|
||||
TooltipAnchor,
|
||||
Label,
|
||||
OGDialogTemplate,
|
||||
Button,
|
||||
Spinner,
|
||||
OGDialog,
|
||||
OGDialogClose,
|
||||
TooltipAnchor,
|
||||
OGDialogTitle,
|
||||
OGDialogHeader,
|
||||
useToastContext,
|
||||
OGDialogContent,
|
||||
} from '@librechat/client';
|
||||
import type { TSharedLinkGetResponse } from 'librechat-data-provider';
|
||||
import {
|
||||
|
|
@ -23,7 +26,6 @@ export default function SharedLinkButton({
|
|||
share,
|
||||
conversationId,
|
||||
targetMessageId,
|
||||
setShareDialogOpen,
|
||||
showQR,
|
||||
setShowQR,
|
||||
setSharedLink,
|
||||
|
|
@ -31,13 +33,13 @@ export default function SharedLinkButton({
|
|||
share: TSharedLinkGetResponse | undefined;
|
||||
conversationId: string;
|
||||
targetMessageId?: string;
|
||||
setShareDialogOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
showQR: boolean;
|
||||
setShowQR: (showQR: boolean) => void;
|
||||
setSharedLink: (sharedLink: string) => void;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const { showToast } = useToastContext();
|
||||
const deleteButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
const [announcement, setAnnouncement] = useState('');
|
||||
const shareId = share?.shareId ?? '';
|
||||
|
|
@ -63,9 +65,16 @@ export default function SharedLinkButton({
|
|||
});
|
||||
|
||||
const deleteMutation = useDeleteSharedLinkMutation({
|
||||
onSuccess: async () => {
|
||||
onSuccess: () => {
|
||||
setShowDeleteDialog(false);
|
||||
setShareDialogOpen(false);
|
||||
setTimeout(() => {
|
||||
const dialog = document
|
||||
.getElementById('share-conversation-dialog')
|
||||
?.closest('[role="dialog"]');
|
||||
if (dialog instanceof HTMLElement) {
|
||||
dialog.focus();
|
||||
}
|
||||
}, 0);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Delete error:', error);
|
||||
|
|
@ -175,6 +184,7 @@ export default function SharedLinkButton({
|
|||
render={(props) => (
|
||||
<Button
|
||||
{...props}
|
||||
ref={deleteButtonRef}
|
||||
onClick={() => setShowDeleteDialog(true)}
|
||||
variant="destructive"
|
||||
aria-label={localize('com_ui_delete')}
|
||||
|
|
@ -185,36 +195,43 @@ export default function SharedLinkButton({
|
|||
/>
|
||||
</div>
|
||||
)}
|
||||
<OGDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<OGDialogTemplate
|
||||
showCloseButton={false}
|
||||
title={localize('com_ui_delete_shared_link_heading')}
|
||||
className="max-w-[450px]"
|
||||
main={
|
||||
<>
|
||||
<div className="flex w-full flex-col items-center gap-2">
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label
|
||||
htmlFor="dialog-confirm-delete"
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
<Trans
|
||||
i18nKey="com_ui_delete_confirm_strong"
|
||||
values={{ title: shareId }}
|
||||
components={{ strong: <strong /> }}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
selection={{
|
||||
selectHandler: handleDelete,
|
||||
selectClasses:
|
||||
'bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 text-white',
|
||||
selectText: localize('com_ui_delete'),
|
||||
}}
|
||||
/>
|
||||
<OGDialog
|
||||
open={showDeleteDialog}
|
||||
triggerRef={deleteButtonRef}
|
||||
onOpenChange={setShowDeleteDialog}
|
||||
>
|
||||
<OGDialogContent className="max-w-[450px]" showCloseButton={false}>
|
||||
<OGDialogHeader>
|
||||
<OGDialogTitle>{localize('com_ui_delete_shared_link_heading')}</OGDialogTitle>
|
||||
</OGDialogHeader>
|
||||
<div className="flex w-full flex-col items-center gap-2">
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label htmlFor="dialog-confirm-delete" className="text-left text-sm font-medium">
|
||||
<Trans
|
||||
i18nKey="com_ui_delete_confirm_strong"
|
||||
values={{ title: shareId }}
|
||||
components={{ strong: <strong /> }}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-4 pt-4">
|
||||
<OGDialogClose asChild>
|
||||
<Button variant="outline">{localize('com_ui_cancel')}</Button>
|
||||
</OGDialogClose>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleDelete}
|
||||
disabled={deleteMutation.isLoading}
|
||||
>
|
||||
{deleteMutation.isLoading ? (
|
||||
<Spinner className="size-4" />
|
||||
) : (
|
||||
localize('com_ui_delete')
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</OGDialogContent>
|
||||
</OGDialog>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue