⌨️ 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:
Danny Avila 2025-12-12 17:18:21 -05:00 committed by GitHub
parent 5b0cce2e2a
commit 4d7e6b4a58
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 670 additions and 219 deletions

View file

@ -1,4 +1,4 @@
import React from 'react';
import React, { useRef, useState, useEffect } from 'react';
import { EarthIcon, Pin, PinOff } from 'lucide-react';
import { isAgentsEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
import { useModelSelectorContext } from '../ModelSelectorContext';
@ -18,6 +18,26 @@ export function EndpointModelItem({ modelId, endpoint, isSelected }: EndpointMod
const { handleSelectModel } = useModelSelectorContext();
const { isFavoriteModel, toggleFavoriteModel, isFavoriteAgent, toggleFavoriteAgent } =
useFavorites();
const itemRef = useRef<HTMLDivElement>(null);
const [isActive, setIsActive] = useState(false);
useEffect(() => {
const element = itemRef.current;
if (!element) {
return;
}
const observer = new MutationObserver(() => {
setIsActive(element.hasAttribute('data-active-item'));
});
observer.observe(element, { attributes: true, attributeFilter: ['data-active-item'] });
setIsActive(element.hasAttribute('data-active-item'));
return () => observer.disconnect();
}, []);
let isGlobal = false;
let modelName = modelId;
const avatarUrl = endpoint?.modelIcons?.[modelId ?? ''] || null;
@ -42,8 +62,7 @@ export function EndpointModelItem({ modelId, endpoint, isSelected }: EndpointMod
? isFavoriteAgent(modelId ?? '')
: isFavoriteModel(modelId ?? '', endpoint.value);
const handleFavoriteClick = (e: React.MouseEvent) => {
e.stopPropagation();
const handleFavoriteToggle = () => {
if (!modelId) {
return;
}
@ -55,6 +74,11 @@ export function EndpointModelItem({ modelId, endpoint, isSelected }: EndpointMod
}
};
const handleFavoriteClick = (e: React.MouseEvent) => {
e.stopPropagation();
handleFavoriteToggle();
};
const renderAvatar = () => {
const isAgentOrAssistant =
isAgentsEndpoint(endpoint.value) || isAssistantsEndpoint(endpoint.value);
@ -84,6 +108,7 @@ export function EndpointModelItem({ modelId, endpoint, isSelected }: EndpointMod
return (
<MenuItem
ref={itemRef}
key={modelId}
onClick={() => handleSelectModel(endpoint, modelId ?? '')}
className="group flex w-full cursor-pointer items-center justify-between rounded-lg px-2 text-sm"
@ -94,20 +119,18 @@ export function EndpointModelItem({ modelId, endpoint, isSelected }: EndpointMod
{isGlobal && <EarthIcon className="ml-1 size-4 text-surface-submit" />}
</div>
<button
tabIndex={isActive ? 0 : -1}
onClick={handleFavoriteClick}
aria-label={isFavorite ? localize('com_ui_unpin') : localize('com_ui_pin')}
className={cn(
'rounded-md p-1 hover:bg-surface-hover',
isFavorite ? 'visible' : 'invisible group-hover:visible',
isFavorite ? 'visible' : 'invisible group-hover:visible group-data-[active-item]:visible',
)}
>
{isFavorite ? (
<PinOff className="h-4 w-4 text-text-secondary" />
) : (
<Pin
className="h-4 w-4 text-text-secondary"
aria-hidden="true"
/>
<Pin className="h-4 w-4 text-text-secondary" aria-hidden="true" />
)}
</button>
{isSelected && (