↕️ feat: Improve Sorting Accessibility in Archived Chats and Shared Links Modals (#10973)

* fix: cast translation keys for ESLint

* fix: sort by header withi keyboard nav now retains focus on press

* fix: focus retained on key press for sorts in archived chat table

* fix: cast translation keys for ESLint
This commit is contained in:
Dustin Healy 2025-12-15 07:28:28 -08:00 committed by GitHub
parent f11817a30e
commit 6ae839c14d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 71 additions and 76 deletions

View file

@ -12,6 +12,7 @@ import {
ExternalLink, ExternalLink,
} from 'lucide-react'; } from 'lucide-react';
import type { SharedLinkItem, SharedLinksListParams } from 'librechat-data-provider'; import type { SharedLinkItem, SharedLinksListParams } from 'librechat-data-provider';
import type { TranslationKeys } from '~/hooks';
import { import {
OGDialog, OGDialog,
useToastContext, useToastContext,
@ -62,14 +63,6 @@ export default function SharedLinks() {
refetchOnMount: false, refetchOnMount: false,
}); });
const handleSort = useCallback((sortField: string, sortOrder: 'asc' | 'desc') => {
setQueryParams((prev) => ({
...prev,
sortBy: sortField as 'title' | 'createdAt',
sortDirection: sortOrder,
}));
}, []);
const handleFilterChange = useCallback((value: string) => { const handleFilterChange = useCallback((value: string) => {
const encodedValue = encodeURIComponent(value.trim()); const encodedValue = encodeURIComponent(value.trim());
setQueryParams((prev) => ({ setQueryParams((prev) => ({
@ -120,7 +113,7 @@ export default function SharedLinks() {
if (validRows.length === 0) { if (validRows.length === 0) {
showToast({ showToast({
message: localize('com_ui_no_valid_items'), message: localize('com_ui_no_valid_items' as TranslationKeys),
severity: NotificationSeverity.WARNING, severity: NotificationSeverity.WARNING,
}); });
return; return;
@ -134,15 +127,15 @@ export default function SharedLinks() {
showToast({ showToast({
message: localize( message: localize(
validRows.length === 1 validRows.length === 1
? 'com_ui_shared_link_delete_success' ? ('com_ui_shared_link_delete_success' as TranslationKeys)
: 'com_ui_shared_link_bulk_delete_success', : ('com_ui_shared_link_bulk_delete_success' as TranslationKeys),
), ),
severity: NotificationSeverity.SUCCESS, severity: NotificationSeverity.SUCCESS,
}); });
} catch (error) { } catch (error) {
console.error('Failed to delete shared links:', error); console.error('Failed to delete shared links:', error);
showToast({ showToast({
message: localize('com_ui_bulk_delete_error'), message: localize('com_ui_bulk_delete_error' as TranslationKeys),
severity: NotificationSeverity.ERROR, severity: NotificationSeverity.ERROR,
}); });
} }
@ -168,26 +161,28 @@ export default function SharedLinks() {
() => [ () => [
{ {
accessorKey: 'title', accessorKey: 'title',
header: () => { header: ({ column }) => {
const isSorted = queryParams.sortBy === 'title'; const sortState = column.getIsSorted();
const sortDirection = queryParams.sortDirection; let SortIcon = ArrowUpDown;
let ariaSort: 'ascending' | 'descending' | 'none' = 'none';
if (sortState === 'desc') {
SortIcon = ArrowDown;
ariaSort = 'descending';
} else if (sortState === 'asc') {
SortIcon = ArrowUp;
ariaSort = 'ascending';
}
return ( return (
<Button <Button
variant="ghost" variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="px-2 py-0 text-xs hover:bg-surface-hover sm:px-2 sm:py-2 sm:text-sm" className="px-2 py-0 text-xs hover:bg-surface-hover sm:px-2 sm:py-2 sm:text-sm"
onClick={() => aria-sort={ariaSort}
handleSort('title', isSorted && sortDirection === 'asc' ? 'desc' : 'asc')
}
aria-label={localize('com_ui_name_sort')} aria-label={localize('com_ui_name_sort')}
aria-current={sortState ? 'true' : 'false'}
> >
{localize('com_ui_name')} {localize('com_ui_name')}
{isSorted && sortDirection === 'asc' && ( <SortIcon className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />
<ArrowUp className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />
)}
{isSorted && sortDirection === 'desc' && (
<ArrowDown className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />
)}
{!isSorted && <ArrowUpDown className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />}
</Button> </Button>
); );
}, },
@ -218,26 +213,28 @@ export default function SharedLinks() {
}, },
{ {
accessorKey: 'createdAt', accessorKey: 'createdAt',
header: () => { header: ({ column }) => {
const isSorted = queryParams.sortBy === 'createdAt'; const sortState = column.getIsSorted();
const sortDirection = queryParams.sortDirection; let SortIcon = ArrowUpDown;
let ariaSort: 'ascending' | 'descending' | 'none' = 'none';
if (sortState === 'desc') {
SortIcon = ArrowDown;
ariaSort = 'descending';
} else if (sortState === 'asc') {
SortIcon = ArrowUp;
ariaSort = 'ascending';
}
return ( return (
<Button <Button
variant="ghost" variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="px-2 py-0 text-xs hover:bg-surface-hover sm:px-2 sm:py-2 sm:text-sm" className="px-2 py-0 text-xs hover:bg-surface-hover sm:px-2 sm:py-2 sm:text-sm"
onClick={() => aria-sort={ariaSort}
handleSort('createdAt', isSorted && sortDirection === 'asc' ? 'desc' : 'asc') aria-label={localize('com_ui_creation_date_sort' as TranslationKeys)}
} aria-current={sortState ? 'true' : 'false'}
aria-label={localize('com_ui_creation_date_sort')}
> >
{localize('com_ui_date')} {localize('com_ui_date')}
{isSorted && sortDirection === 'asc' && ( <SortIcon className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />
<ArrowUp className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />
)}
{isSorted && sortDirection === 'desc' && (
<ArrowDown className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />
)}
{!isSorted && <ArrowUpDown className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />}
</Button> </Button>
); );
}, },
@ -300,7 +297,7 @@ export default function SharedLinks() {
), ),
}, },
], ],
[isSmallScreen, localize, queryParams, handleSort], [isSmallScreen, localize],
); );
return ( return (

View file

@ -17,6 +17,7 @@ import {
OGDialogContent, OGDialogContent,
} from '@librechat/client'; } from '@librechat/client';
import type { ConversationListParams, TConversation } from 'librechat-data-provider'; import type { ConversationListParams, TConversation } from 'librechat-data-provider';
import type { TranslationKeys } from '~/hooks';
import { import {
useConversationsInfiniteQuery, useConversationsInfiniteQuery,
useDeleteConversationMutation, useDeleteConversationMutation,
@ -56,14 +57,6 @@ export default function ArchivedChatsTable({
refetchOnMount: false, refetchOnMount: false,
}); });
const handleSort = useCallback((sortField: string, sortOrder: 'asc' | 'desc') => {
setQueryParams((prev) => ({
...prev,
sortBy: sortField as 'title' | 'createdAt',
sortDirection: sortOrder,
}));
}, []);
const handleFilterChange = useCallback((value: string) => { const handleFilterChange = useCallback((value: string) => {
const encodedValue = encodeURIComponent(value.trim()); const encodedValue = encodeURIComponent(value.trim());
setQueryParams((prev) => ({ setQueryParams((prev) => ({
@ -133,25 +126,28 @@ export default function ArchivedChatsTable({
() => [ () => [
{ {
accessorKey: 'title', accessorKey: 'title',
header: () => { header: ({ column }) => {
const isSorted = queryParams.sortBy === 'title'; const sortState = column.getIsSorted();
const sortDirection = queryParams.sortDirection; let SortIcon = ArrowUpDown;
let ariaSort: 'ascending' | 'descending' | 'none' = 'none';
if (sortState === 'desc') {
SortIcon = ArrowDown;
ariaSort = 'descending';
} else if (sortState === 'asc') {
SortIcon = ArrowUp;
ariaSort = 'ascending';
}
return ( return (
<Button <Button
variant="ghost" variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="px-2 py-0 text-xs hover:bg-surface-hover sm:px-2 sm:py-2 sm:text-sm" className="px-2 py-0 text-xs hover:bg-surface-hover sm:px-2 sm:py-2 sm:text-sm"
onClick={() => aria-sort={ariaSort}
handleSort('title', isSorted && sortDirection === 'asc' ? 'desc' : 'asc') aria-label={localize('com_nav_archive_name_sort' as TranslationKeys)}
} aria-current={sortState ? 'true' : 'false'}
> >
{localize('com_nav_archive_name')} {localize('com_nav_archive_name')}
{isSorted && sortDirection === 'asc' && ( <SortIcon className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />
<ArrowUp className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />
)}
{isSorted && sortDirection === 'desc' && (
<ArrowDown className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />
)}
{!isSorted && <ArrowUpDown className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />}
</Button> </Button>
); );
}, },
@ -180,26 +176,28 @@ export default function ArchivedChatsTable({
}, },
{ {
accessorKey: 'createdAt', accessorKey: 'createdAt',
header: () => { header: ({ column }) => {
const isSorted = queryParams.sortBy === 'createdAt'; const sortState = column.getIsSorted();
const sortDirection = queryParams.sortDirection; let SortIcon = ArrowUpDown;
let ariaSort: 'ascending' | 'descending' | 'none' = 'none';
if (sortState === 'desc') {
SortIcon = ArrowDown;
ariaSort = 'descending';
} else if (sortState === 'asc') {
SortIcon = ArrowUp;
ariaSort = 'ascending';
}
return ( return (
<Button <Button
variant="ghost" variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className="px-2 py-0 text-xs hover:bg-surface-hover sm:px-2 sm:py-2 sm:text-sm" className="px-2 py-0 text-xs hover:bg-surface-hover sm:px-2 sm:py-2 sm:text-sm"
onClick={() => aria-sort={ariaSort}
handleSort('createdAt', isSorted && sortDirection === 'asc' ? 'desc' : 'asc') aria-label={localize('com_nav_archive_created_at_sort' as TranslationKeys)}
} aria-current={sortState ? 'true' : 'false'}
aria-label={localize('com_nav_archive_created_at_sort')}
> >
{localize('com_nav_archive_created_at')} {localize('com_nav_archive_created_at')}
{isSorted && sortDirection === 'asc' && ( <SortIcon className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />
<ArrowUp className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />
)}
{isSorted && sortDirection === 'desc' && (
<ArrowDown className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />
)}
{!isSorted && <ArrowUpDown className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />}
</Button> </Button>
); );
}, },
@ -270,7 +268,7 @@ export default function ArchivedChatsTable({
}, },
}, },
], ],
[handleSort, isSmallScreen, localize, queryParams, unarchiveMutation], [isSmallScreen, localize, unarchiveMutation],
); );
return ( return (