mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 00:40:14 +01:00
🛠️ fix: Improve Accessibility and Display of Conversation Menu (#6913)
* 📦 chore: update @ariakit/react-core to version 0.4.17 in package.json and package-lock.json
* refactor: add additional ariakit menu props and unmount menu if state changes
* fix: accessibility issues and incompatibility issues due to non-portaled menu
* fix: improve visibility and accessibility of conversation options, making sure to expand dynamically when becoming active
* fix: adjust max width for conversation options popover to improve visibility
This commit is contained in:
parent
000f3a3733
commit
16aa5ed466
5 changed files with 153 additions and 113 deletions
|
|
@ -29,7 +29,7 @@
|
|||
"homepage": "https://librechat.ai",
|
||||
"dependencies": {
|
||||
"@ariakit/react": "^0.4.15",
|
||||
"@ariakit/react-core": "^0.4.15",
|
||||
"@ariakit/react-core": "^0.4.17",
|
||||
"@codesandbox/sandpack-react": "^2.19.10",
|
||||
"@dicebear/collection": "^9.2.2",
|
||||
"@dicebear/core": "^9.2.2",
|
||||
|
|
|
|||
|
|
@ -184,11 +184,12 @@ export default function Conversation({
|
|||
)}
|
||||
<div
|
||||
className={cn(
|
||||
'mr-2',
|
||||
'mr-2 flex origin-left',
|
||||
isPopoverActive || isActiveConvo
|
||||
? 'flex'
|
||||
: 'hidden group-focus-within:flex group-hover:flex',
|
||||
? 'pointer-events-auto max-w-[28px] scale-x-100 opacity-100'
|
||||
: 'pointer-events-none max-w-0 scale-x-0 opacity-0 group-focus-within:pointer-events-auto group-focus-within:max-w-[28px] group-focus-within:scale-x-100 group-focus-within:opacity-100 group-hover:pointer-events-auto group-hover:max-w-[28px] group-hover:scale-x-100 group-hover:opacity-100',
|
||||
)}
|
||||
aria-hidden={!(isPopoverActive || isActiveConvo)}
|
||||
>
|
||||
{!renaming && <ConvoOptions {...convoOptionsProps} />}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -50,35 +50,6 @@ function ConvoOptions({
|
|||
|
||||
const archiveConvoMutation = useArchiveConvoMutation();
|
||||
|
||||
const archiveHandler = async () => {
|
||||
const convoId = conversationId ?? '';
|
||||
|
||||
if (!convoId) {
|
||||
return;
|
||||
}
|
||||
|
||||
archiveConvoMutation.mutate(
|
||||
{ conversationId: convoId, isArchived: true },
|
||||
{
|
||||
onSuccess: () => {
|
||||
if (currentConvoId === convoId || currentConvoId === 'new') {
|
||||
newConversation();
|
||||
navigate('/c/new', { replace: true });
|
||||
}
|
||||
retainView();
|
||||
setIsPopoverActive(false);
|
||||
},
|
||||
onError: () => {
|
||||
showToast({
|
||||
message: localize('com_ui_archive_error'),
|
||||
severity: NotificationSeverity.ERROR,
|
||||
showIcon: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const duplicateConversation = useDuplicateConversationMutation({
|
||||
onSuccess: (data) => {
|
||||
navigateToConvo(data.conversation);
|
||||
|
|
@ -220,6 +191,10 @@ function ConvoOptions({
|
|||
return (
|
||||
<>
|
||||
<DropdownPopup
|
||||
portal={true}
|
||||
mountByState={true}
|
||||
unmountOnHide={true}
|
||||
preserveTabOrder={true}
|
||||
isOpen={isPopoverActive}
|
||||
setIsOpen={setIsPopoverActive}
|
||||
trigger={
|
||||
|
|
@ -227,12 +202,19 @@ function ConvoOptions({
|
|||
id={`conversation-menu-${conversationId}`}
|
||||
aria-label={localize('com_nav_convo_menu_options')}
|
||||
className={cn(
|
||||
'z-30 inline-flex h-7 w-7 items-center justify-center gap-2 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:pointer-events-none disabled:opacity-50',
|
||||
isActiveConvo === true
|
||||
'z-30 inline-flex h-7 w-7 items-center justify-center gap-2 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
|
||||
? 'opacity-100'
|
||||
: 'opacity-0 focus:opacity-100 group-focus-within:opacity-100 group-hover:opacity-100 data-[open]:opacity-100',
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onClick={(e: MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onKeyDown={(e: React.KeyboardEvent<HTMLButtonElement>) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Ellipsis className="icon-md text-text-secondary" aria-hidden={true} />
|
||||
</Menu.MenuButton>
|
||||
|
|
|
|||
|
|
@ -16,36 +16,72 @@ interface DropdownProps {
|
|||
anchor?: { x: string; y: string };
|
||||
gutter?: number;
|
||||
modal?: boolean;
|
||||
portal?: boolean;
|
||||
preserveTabOrder?: boolean;
|
||||
focusLoop?: boolean;
|
||||
menuId: string;
|
||||
mountByState?: boolean;
|
||||
unmountOnHide?: boolean;
|
||||
finalFocus?: React.RefObject<HTMLElement>;
|
||||
}
|
||||
|
||||
type MenuProps = Omit<
|
||||
DropdownProps,
|
||||
'trigger' | 'isOpen' | 'setIsOpen' | 'focusLoop' | 'mountByState'
|
||||
>;
|
||||
|
||||
const DropdownPopup: React.FC<DropdownProps> = ({
|
||||
keyPrefix,
|
||||
trigger,
|
||||
items,
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
menuId,
|
||||
modal,
|
||||
gutter = 8,
|
||||
sameWidth,
|
||||
className,
|
||||
focusLoop,
|
||||
iconClassName,
|
||||
itemClassName,
|
||||
mountByState,
|
||||
...props
|
||||
}) => {
|
||||
const menu = Ariakit.useMenuStore({ open: isOpen, setOpen: setIsOpen, focusLoop });
|
||||
|
||||
if (mountByState) {
|
||||
return (
|
||||
<Ariakit.MenuProvider store={menu}>
|
||||
{trigger}
|
||||
{isOpen && <Menu {...props} />}
|
||||
</Ariakit.MenuProvider>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Ariakit.MenuProvider store={menu}>
|
||||
{trigger}
|
||||
<Menu {...props} />
|
||||
</Ariakit.MenuProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const Menu: React.FC<MenuProps> = ({
|
||||
items,
|
||||
menuId,
|
||||
keyPrefix,
|
||||
className,
|
||||
iconClassName,
|
||||
itemClassName,
|
||||
modal,
|
||||
portal,
|
||||
sameWidth,
|
||||
gutter = 8,
|
||||
finalFocus,
|
||||
unmountOnHide,
|
||||
preserveTabOrder,
|
||||
}) => {
|
||||
const menu = Ariakit.useMenuContext();
|
||||
return (
|
||||
<Ariakit.Menu
|
||||
id={menuId}
|
||||
className={cn('popover-ui z-50', className)}
|
||||
gutter={gutter}
|
||||
modal={modal}
|
||||
gutter={gutter}
|
||||
portal={portal}
|
||||
sameWidth={sameWidth}
|
||||
finalFocus={finalFocus}
|
||||
unmountOnHide={unmountOnHide}
|
||||
preserveTabOrder={preserveTabOrder}
|
||||
className={cn('popover-ui z-50', className)}
|
||||
>
|
||||
{items
|
||||
.filter((item) => item.show !== false)
|
||||
|
|
@ -55,7 +91,7 @@ const DropdownPopup: React.FC<DropdownProps> = ({
|
|||
}
|
||||
return (
|
||||
<Ariakit.MenuItem
|
||||
key={`${keyPrefix ?? ''}${index}`}
|
||||
key={`${keyPrefix ?? ''}${index}-${item.id ?? ''}`}
|
||||
id={item.id}
|
||||
className={cn(
|
||||
'group flex w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-3.5 text-sm text-text-primary outline-none transition-colors duration-200 hover:bg-surface-hover focus:bg-surface-hover md:px-2.5 md:py-2',
|
||||
|
|
@ -73,7 +109,7 @@ const DropdownPopup: React.FC<DropdownProps> = ({
|
|||
if (item.hideOnClick === false) {
|
||||
return;
|
||||
}
|
||||
menu.hide();
|
||||
menu?.hide();
|
||||
}}
|
||||
>
|
||||
{item.icon != null && (
|
||||
|
|
@ -92,7 +128,6 @@ const DropdownPopup: React.FC<DropdownProps> = ({
|
|||
);
|
||||
})}
|
||||
</Ariakit.Menu>
|
||||
</Ariakit.MenuProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
52
package-lock.json
generated
52
package-lock.json
generated
|
|
@ -1165,7 +1165,7 @@
|
|||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@ariakit/react": "^0.4.15",
|
||||
"@ariakit/react-core": "^0.4.15",
|
||||
"@ariakit/react-core": "^0.4.17",
|
||||
"@codesandbox/sandpack-react": "^2.19.10",
|
||||
"@dicebear/collection": "^9.2.2",
|
||||
"@dicebear/core": "^9.2.2",
|
||||
|
|
@ -1299,6 +1299,42 @@
|
|||
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"client/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==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ariakit/core": "0.4.15",
|
||||
"@floating-ui/dom": "^1.0.0",
|
||||
"use-sync-external-store": "^1.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"client/node_modules/@ariakit/react-core/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==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"client/node_modules/@ariakit/react/node_modules/@ariakit/react-core": {
|
||||
"version": "0.4.15",
|
||||
"resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.15.tgz",
|
||||
"integrity": "sha512-Up8+U97nAPJdyUh9E8BCEhJYTA+eVztWpHoo1R9zZfHd4cnBWAg5RHxEmMH+MamlvuRxBQA71hFKY/735fDg+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ariakit/core": "0.4.14",
|
||||
"@floating-ui/dom": "^1.0.0",
|
||||
"use-sync-external-store": "^1.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"client/node_modules/@babel/compat-data": {
|
||||
"version": "7.26.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz",
|
||||
|
|
@ -3417,20 +3453,6 @@
|
|||
"resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.14.tgz",
|
||||
"integrity": "sha512-hpzZvyYzGhP09S9jW1XGsU/FD5K3BKsH1eG/QJ8rfgEeUdPS7BvHPt5lHbOeJ2cMrRzBEvsEzLi1ivfDifHsVA=="
|
||||
},
|
||||
"node_modules/@ariakit/react-core": {
|
||||
"version": "0.4.15",
|
||||
"resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.15.tgz",
|
||||
"integrity": "sha512-Up8+U97nAPJdyUh9E8BCEhJYTA+eVztWpHoo1R9zZfHd4cnBWAg5RHxEmMH+MamlvuRxBQA71hFKY/735fDg+A==",
|
||||
"dependencies": {
|
||||
"@ariakit/core": "0.4.14",
|
||||
"@floating-ui/dom": "^1.0.0",
|
||||
"use-sync-external-store": "^1.2.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/@aws-crypto/crc32": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-3.0.0.tgz",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue