From 465cb6e394e10331ff21e1e0d82ddf279994ee81 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 21 Jun 2026 12:53:24 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=91=90=20a11y:=20Bump=20`@ariakit/react`,?= =?UTF-8?q?=20Improve=20a11y=20of=20Token=20Usage,=20Archived=20Chats,=20R?= =?UTF-8?q?educe=20Table=20Layout=20Shifts=20(#13874)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: Update `@ariakit/react` and `@ariakit/react-core` dependencies to v0.4.29 and v0.4.26 respectively, and add new `@ariakit/components`, `@ariakit/react-components`, `@ariakit/react-store`, and `@ariakit/react-utils` packages to package-lock.json and package.json files. * fix: restore keyboard navigation for Tools dropdown submenus Compose the Artifacts and MCP submenu triggers as a `MenuButton` that receives the parent `MenuItem`'s props/ref directly, instead of nesting a `MenuItem` inside the submenu's own provider and placing the ref on a wrapper div. This registers the focusable trigger with the parent menu store so arrow-key navigation reaches the items, which fully broke under Ariakit 0.4.29. * fix: Improve keyboard navigation for TokenUsageIndicator popover Refactor the TokenUsageIndicator component to enhance keyboard accessibility. The popover now maintains focus on the gauge trigger, ensuring that the Escape key closes the popover without shifting focus to the non-interactive panel. Additionally, the autoFocusOnShow property is set to false to prevent unwanted focus behavior when the popover is displayed. * fix: Stabilize focus and layout shift in Archived Chats dialog Anchor dialog focus to the content element so rapid tabbing during the virtualized table's loading state no longer escapes to the page's top focus guard, and stabilize the columns memo to keep the focus trap intact. Reserve a fixed height and stable scrollbar gutter, and drop the redundant nested scroll wrapper in the shared DataTable to eliminate load-time layout shift. * fix: Add stable scrollbar gutter to SharedLinks DataTable Enhance the layout stability of the SharedLinks component by adding a "scrollbar-gutter-stable" class to the DataTable. This change aims to prevent layout shifts during loading, improving the overall user experience. * fix: Enhance keyboard accessibility and focus management in TokenUsageIndicator Refactor the TokenUsageIndicator component to improve keyboard navigation and focus behavior. Introduced a useRef hook for the disclosure button to ensure focus remains on the gauge trigger when the popover is opened. Updated the popover's finalFocus property to return focus to the trigger on close, enhancing the overall user experience for keyboard users. --- client/package.json | 4 +- .../Chat/Input/ArtifactsSubMenu.tsx | 44 +++---- .../src/components/Chat/Input/MCPSubMenu.tsx | 33 +++--- .../Chat/Input/TokenUsage/index.tsx | 10 +- .../Nav/SettingsTabs/Data/SharedLinks.tsx | 1 + .../General/ArchivedChatsModal.tsx | 15 ++- .../General/ArchivedChatsTable.tsx | 45 ++++---- package-lock.json | 109 +++++++++++++++--- packages/client/package.json | 4 +- packages/client/src/components/DataTable.tsx | 5 +- 10 files changed, 184 insertions(+), 86 deletions(-) diff --git a/client/package.json b/client/package.json index d54dfd285f..0ed17ee1a9 100644 --- a/client/package.json +++ b/client/package.json @@ -29,8 +29,8 @@ }, "homepage": "https://librechat.ai", "dependencies": { - "@ariakit/react": "^0.4.15", - "@ariakit/react-core": "^0.4.17", + "@ariakit/react": "^0.4.29", + "@ariakit/react-core": "^0.4.26", "@codesandbox/sandpack-react": "^2.19.10", "@dicebear/collection": "^9.4.1", "@dicebear/core": "^9.4.1", diff --git a/client/src/components/Chat/Input/ArtifactsSubMenu.tsx b/client/src/components/Chat/Input/ArtifactsSubMenu.tsx index b968c0ab55..2166cb669a 100644 --- a/client/src/components/Chat/Input/ArtifactsSubMenu.tsx +++ b/client/src/components/Chat/Input/ArtifactsSubMenu.tsx @@ -1,12 +1,12 @@ import React from 'react'; import * as Ariakit from '@ariakit/react'; import { PinIcon } from '@librechat/client'; -import { ChevronRight, WandSparkles } from 'lucide-react'; import { ArtifactModes } from 'librechat-data-provider'; +import { ChevronRight, WandSparkles } from 'lucide-react'; import { useLocalize } from '~/hooks'; import { cn } from '~/utils'; -interface ArtifactsSubMenuProps { +interface ArtifactsSubMenuProps extends React.HTMLAttributes { isArtifactsPinned: boolean; setIsArtifactsPinned: (value: boolean) => void; artifactsMode: string; @@ -15,7 +15,7 @@ interface ArtifactsSubMenuProps { handleCustomToggle: () => void; } -const ArtifactsSubMenu = React.forwardRef( +const ArtifactsSubMenu = React.forwardRef( ( { isArtifactsPinned, @@ -24,6 +24,7 @@ const ArtifactsSubMenu = React.forwardRef handleArtifactsToggle, handleShadcnToggle, handleCustomToggle, + className, ...props }, ref, @@ -41,25 +42,24 @@ const ArtifactsSubMenu = React.forwardRef const isCustomEnabled = artifactsMode === ArtifactModes.CUSTOM; return ( -
+ <> - ) => { - e.stopPropagation(); - handleArtifactsToggle(); - }} - onMouseEnter={() => { - if (isEnabled) { - menuStore.show(); - } - }} - className="flex w-full cursor-pointer items-center justify-between rounded-lg p-2 hover:bg-surface-hover" - /> - } + onClick={(e: React.MouseEvent) => { + e.stopPropagation(); + handleArtifactsToggle(); + }} + onMouseEnter={() => { + if (isEnabled) { + menuStore.show(); + } + }} + className={cn( + 'flex w-full cursor-pointer items-center justify-between rounded-lg p-2 hover:bg-surface-hover', + className, + )} >
-
+ {isEnabled && ( )}
-
+ ); }, ); diff --git a/client/src/components/Chat/Input/MCPSubMenu.tsx b/client/src/components/Chat/Input/MCPSubMenu.tsx index a1ed542d97..c66a6d9df2 100644 --- a/client/src/components/Chat/Input/MCPSubMenu.tsx +++ b/client/src/components/Chat/Input/MCPSubMenu.tsx @@ -8,12 +8,12 @@ import { useBadgeRowContext } from '~/Providers'; import { useLocalize } from '~/hooks'; import { cn } from '~/utils'; -interface MCPSubMenuProps { +interface MCPSubMenuProps extends React.HTMLAttributes { placeholder?: string; } -const MCPSubMenu = React.forwardRef( - ({ placeholder, ...props }, ref) => { +const MCPSubMenu = React.forwardRef( + ({ placeholder, className, ...props }, ref) => { const localize = useLocalize(); const context = useBadgeRowContext(); const { storageContextKey, mcpServerManager } = context ?? {}; @@ -48,20 +48,19 @@ const MCPSubMenu = React.forwardRef( const configDialogProps = getConfigDialogProps(); return ( -
+ <> - ) => { - e.stopPropagation(); - menuStore.toggle(); - }} - className="flex w-full cursor-pointer items-center justify-between rounded-lg p-2 hover:bg-surface-hover" - /> - } + onClick={(e: React.MouseEvent) => { + e.stopPropagation(); + menuStore.toggle(); + }} + className={cn( + 'flex w-full cursor-pointer items-center justify-between rounded-lg p-2 hover:bg-surface-hover', + className, + )} >
-
+ ( {configDialogProps && ( )} -
+ ); }, ); diff --git a/client/src/components/Chat/Input/TokenUsage/index.tsx b/client/src/components/Chat/Input/TokenUsage/index.tsx index cb9f4894a5..ff969148d5 100644 --- a/client/src/components/Chat/Input/TokenUsage/index.tsx +++ b/client/src/components/Chat/Input/TokenUsage/index.tsx @@ -1,4 +1,4 @@ -import { memo } from 'react'; +import { memo, useRef } from 'react'; import * as Ariakit from '@ariakit/react'; import { TooltipAnchor } from '@librechat/client'; import type { TConversation } from 'librechat-data-provider'; @@ -29,6 +29,7 @@ function TokenUsageIndicator({ const localize = useLocalize(); const view = useTokenUsage({ index, conversation, isSubmitting }); const popover = Ariakit.usePopoverStore({ placement: 'top' }); + const disclosureRef = useRef(null); /** Hide until the branch has data — keeps a fresh, message-less chat clean and * lets the indicator animate into view once the first tokens land. */ @@ -64,6 +65,7 @@ function TokenUsageIndicator({ side="top" render={ } /> + {/* Focus the labelled dialog on open so screen readers enter and announce + the breakdown, and so focus stays contained instead of falling back to + the body (which the composer's global focus logic would steal). The + visible ring is suppressed via focus:outline-none, and finalFocus + returns focus to the gauge trigger on close. */} diff --git a/client/src/components/Nav/SettingsTabs/Data/SharedLinks.tsx b/client/src/components/Nav/SettingsTabs/Data/SharedLinks.tsx index 5413bcb68c..0eaaed2088 100644 --- a/client/src/components/Nav/SettingsTabs/Data/SharedLinks.tsx +++ b/client/src/components/Nav/SettingsTabs/Data/SharedLinks.tsx @@ -332,6 +332,7 @@ export default function SharedLinks() { ; }) { const localize = useLocalize(); + const contentRef = useRef(null); + + /** The virtualized table has no stable focusable on mount, so Radix's default + * autofocus lands on a row that the virtualizer tears out, dropping focus to + * the page's top focus guard; anchor focus to the dialog content instead. */ + const handleOpenAutoFocus = (event: Event) => { + event.preventDefault(); + contentRef.current?.focus(); + }; return ( {localize('com_nav_archived_chats')} diff --git a/client/src/components/Nav/SettingsTabs/General/ArchivedChatsTable.tsx b/client/src/components/Nav/SettingsTabs/General/ArchivedChatsTable.tsx index 46752da5d1..15ac9dc456 100644 --- a/client/src/components/Nav/SettingsTabs/General/ArchivedChatsTable.tsx +++ b/client/src/components/Nav/SettingsTabs/General/ArchivedChatsTable.tsx @@ -109,7 +109,7 @@ export default function ArchivedChatsTable({ }, }); - const unarchiveMutation = useArchiveConvoMutation({ + const { mutate: unarchiveConversation, isLoading: isUnarchiving } = useArchiveConvoMutation({ onSuccess: async () => { await refetch(); }, @@ -253,20 +253,16 @@ export default function ArchivedChatsTable({ variant="ghost" className="h-8 w-8 p-0 hover:bg-surface-hover" onClick={() => - unarchiveMutation.mutate({ + unarchiveConversation({ conversationId: conversation.conversationId, isArchived: false, }) } title={localize('com_ui_unarchive_conversation')} aria-label={localize('com_ui_unarchive_conversation')} - disabled={unarchiveMutation.isLoading} + disabled={isUnarchiving} > - {unarchiveMutation.isLoading ? ( - - ) : ( - - )} + {isUnarchiving ? : } } /> @@ -296,24 +292,29 @@ export default function ArchivedChatsTable({ }, }, ], - [isSmallScreen, localize, unarchiveMutation], + [isSmallScreen, localize, unarchiveConversation, isUnarchiving], ); return ( <> - + {/* Fixed height keeps the loading (skeleton) and loaded states the same + size, so the virtualized table can't reflow the dialog on load. */} +
+ +
=8" } }, + "node_modules/@ariakit/components": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@ariakit/components/-/components-0.1.2.tgz", + "integrity": "sha512-tvh2P0x1cJnoPXnmDEJwdRk3z7x6cTB8ArctcZdAUXlRg9tuwW/rJoBFJMzD5qMI9CDDlQ3Zctx58HvENw4BYw==", + "license": "MIT", + "dependencies": { + "@ariakit/store": "0.1.2", + "@ariakit/utils": "0.1.2" + } + }, "node_modules/@ariakit/core": { - "version": "0.4.15", - "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.15.tgz", - "integrity": "sha512-vvxmZvkNhiisKM+Y1TbGMUfVVchV/sWu9F0xw0RYADXcimWPK31dd9JnIZs/OQ5pwAryAHmERHwuGQVESkSjwQ==", + "version": "0.4.20", + "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.20.tgz", + "integrity": "sha512-DJbUnui0fM+2ZgiWLOMuFOmlWSJDNV3f6tqghIYRTWEm51TN/LoU6uM8og6/g7Nrwl4Uo5l8AoQT9Kkr/i/uRg==", + "deprecated": "This package has been split into smaller packages. Use @ariakit/components, @ariakit/store, or @ariakit/utils depending on the APIs you need.", "license": "MIT" }, "node_modules/@ariakit/react": { - "version": "0.4.17", - "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.17.tgz", - "integrity": "sha512-HQaIboE2axtlncJz1hRTaiQfJ1GGjhdtNcAnPwdjvl2RybfmlHowIB+HTVBp36LzroKPs/M4hPCxk7XTaqRZGg==", + "version": "0.4.29", + "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.29.tgz", + "integrity": "sha512-SLXlsddWHSwfUol4Yi0zULlalNWjzWjpS3zg7B7aaPd64saONQ5ktWf9KMxqBklcpjMLeF2dB9BAHAvpPVdCIQ==", "license": "MIT", "dependencies": { - "@ariakit/react-core": "0.4.17" + "@ariakit/react-components": "0.1.2" }, "funding": { "type": "opencollective", @@ -785,21 +796,83 @@ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/@ariakit/react-core": { - "version": "0.4.17", - "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.17.tgz", - "integrity": "sha512-kFF6n+gC/5CRQIyaMTFoBPio2xUe0k9rZhMNdUobWRmc/twfeLVkODx+8UVYaNyKilTge8G0JFqwvFKku/jKEw==", + "node_modules/@ariakit/react-components": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@ariakit/react-components/-/react-components-0.1.2.tgz", + "integrity": "sha512-SM+SPMAVlOZmGAfWNBza+0k9y4mkA5/dJhDoOyhE96cbNARy665uLdwowSJl1JGuFfcZzuzAwGon7f/rYeyfkQ==", "license": "MIT", "dependencies": { - "@ariakit/core": "0.4.15", - "@floating-ui/dom": "^1.0.0", - "use-sync-external-store": "^1.2.0" + "@ariakit/components": "0.1.2", + "@ariakit/react-store": "0.1.2", + "@ariakit/react-utils": "0.1.2", + "@ariakit/store": "0.1.2", + "@ariakit/utils": "0.1.2", + "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/@ariakit/react-core": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.26.tgz", + "integrity": "sha512-/Peh1KiVpjj79nCJIa6lEdzSTT9P9FZoy+CxByIFKL3YKdlXmDIIhS1E/tAqKbDq4ODVdynnqmrIDxE5wCoZYw==", + "deprecated": "This package has been split into smaller packages. Use @ariakit/react-components or @ariakit/react-utils depending on the APIs you need.", + "license": "MIT", + "dependencies": { + "@ariakit/core": "0.4.20", + "@floating-ui/dom": "^1.0.0", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@ariakit/react-store": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@ariakit/react-store/-/react-store-0.1.2.tgz", + "integrity": "sha512-1r1Gn0tqhnOS0LFvHNGzn5/8C5aOANO5vb0Gxh94oR/be4zwCSE2zfQjOjRfpL+BBDhOcProME2+G6UslEJxbg==", + "license": "MIT", + "dependencies": { + "@ariakit/react-utils": "0.1.2", + "@ariakit/store": "0.1.2", + "@ariakit/utils": "0.1.2", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@ariakit/react-utils": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@ariakit/react-utils/-/react-utils-0.1.2.tgz", + "integrity": "sha512-Rnl6D1542Mqu80xK++oUv1JXS0PtNmKXd9nkdud5nyvySiBDTrmPqRW44/D+5GbuZrboreQuY3tPYwKL7a7onQ==", + "license": "MIT", + "dependencies": { + "@ariakit/store": "0.1.2", + "@ariakit/utils": "0.1.2" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@ariakit/store": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@ariakit/store/-/store-0.1.2.tgz", + "integrity": "sha512-SS7bV4+a+1q9M9i0WV6DD4P/ypRKlCvII8soo2UMe1yuaxZA/Fc0htHe+EZwjJ6TMLjHfHh2TDSnXyrjC7QImA==", + "license": "MIT", + "dependencies": { + "@ariakit/utils": "0.1.2" + } + }, + "node_modules/@ariakit/utils": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@ariakit/utils/-/utils-0.1.2.tgz", + "integrity": "sha512-lBJhtBWpKjIck/9i7G8cahvaUgLsyGklI/Pjv+VtY9KTzyuzX5GpRbbLKMS/e1qLnFPS4C3CybYB70b1bVcAkw==", + "license": "MIT" + }, "node_modules/@asamuzakjp/css-color": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", @@ -43076,8 +43149,8 @@ "typescript": "^5.9.3" }, "peerDependencies": { - "@ariakit/react": "^0.4.16", - "@ariakit/react-core": "^0.4.17", + "@ariakit/react": "^0.4.29", + "@ariakit/react-core": "^0.4.26", "@dicebear/collection": "^9.4.1", "@dicebear/core": "^9.4.1", "@headlessui/react": "^2.1.2", diff --git a/packages/client/package.json b/packages/client/package.json index 17674b9cc4..ad30791801 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -36,8 +36,8 @@ "dev": "tsdown --watch" }, "peerDependencies": { - "@ariakit/react": "^0.4.16", - "@ariakit/react-core": "^0.4.17", + "@ariakit/react": "^0.4.29", + "@ariakit/react-core": "^0.4.26", "@dicebear/collection": "^9.4.1", "@dicebear/core": "^9.4.1", "@headlessui/react": "^2.1.2", diff --git a/packages/client/src/components/DataTable.tsx b/packages/client/src/components/DataTable.tsx index 09136ac33a..9b3712aa81 100644 --- a/packages/client/src/components/DataTable.tsx +++ b/packages/client/src/components/DataTable.tsx @@ -435,7 +435,10 @@ export default function DataTable({ className, )} > - +
{table.getHeaderGroups().map((headerGroup) => (