mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-21 02:40:14 +01:00
👐 a11y: Accessible Conversation Menu Options (#3864)
* fix: type issues * feat: Fix document title setting in Conversation component * style: new chat theme * fix: No keyboard access to chat menus in the chat history #3788 * fix: Menu button in the chat history area does not indicate its state #3823 * refactor: use ariakit for DropdownPopup * style: update sticky z-index in NewChat component * style: update ConvoOptions menu button styling
This commit is contained in:
parent
2ce4f66218
commit
0a359aa705
6 changed files with 122 additions and 131 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react';
|
||||
import * as Ariakit from '@ariakit/react';
|
||||
|
||||
interface DropdownProps {
|
||||
trigger: React.ReactNode;
|
||||
|
|
@ -15,89 +15,55 @@ interface DropdownProps {
|
|||
isOpen: boolean;
|
||||
setIsOpen: (isOpen: boolean) => void;
|
||||
className?: string;
|
||||
anchor?: string;
|
||||
anchor?: { x: string; y: string };
|
||||
menuId: string;
|
||||
}
|
||||
|
||||
const DropdownPopup: React.FC<DropdownProps> = ({
|
||||
trigger,
|
||||
items,
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
className,
|
||||
anchor = { x: 'bottom', y: 'start' },
|
||||
}) => {
|
||||
const handleButtonClick = () => {
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
const DropdownPopup: React.FC<DropdownProps> = ({ trigger, items, isOpen, setIsOpen, menuId }) => {
|
||||
const menu = Ariakit.useMenuStore({ open: isOpen, setOpen: setIsOpen });
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<MenuButton
|
||||
onClick={handleButtonClick}
|
||||
className={`inline-flex items-center gap-2 rounded-md ${className}`}
|
||||
/** This is set as `div` since triggers themselves are buttons;
|
||||
* prevents React Warning: validateDOMNesting(...): <button> cannot appear as a descendant of <button>.
|
||||
*/
|
||||
as="div"
|
||||
>
|
||||
{trigger}
|
||||
</MenuButton>
|
||||
<Transition
|
||||
show={open}
|
||||
enter="transition-opacity duration-150"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity duration-150"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
afterLeave={() => setIsOpen(false)}
|
||||
>
|
||||
<div className={`${isOpen ? 'visible' : 'invisible'}`}>
|
||||
{open && (
|
||||
<MenuItems
|
||||
static
|
||||
// @ts-ignore
|
||||
anchor={anchor}
|
||||
className="mt-2 overflow-hidden rounded-lg bg-header-primary p-1.5 shadow-lg outline-none focus-visible:ring-2 focus-visible:ring-ring-primary"
|
||||
>
|
||||
<div>
|
||||
{items
|
||||
.filter((item) => item.show !== false)
|
||||
.map((item, index) =>
|
||||
item.separate ? (
|
||||
<div key={index} className="my-1 h-px bg-white/10" />
|
||||
) : (
|
||||
<MenuItem key={index}>
|
||||
<button
|
||||
onClick={item.onClick}
|
||||
className="group flex w-full gap-2 rounded-lg p-2.5 text-sm text-text-primary transition-colors duration-200 data-[focus]:bg-surface-hover"
|
||||
disabled={item.disabled}
|
||||
>
|
||||
{item.icon && (
|
||||
<span className="mr-2 h-5 w-5" aria-hidden="true">
|
||||
{item.icon}
|
||||
</span>
|
||||
)}
|
||||
{item.label}
|
||||
{item.kbd && (
|
||||
<kbd className="ml-auto hidden font-sans text-xs text-black/50 group-data-[focus]:inline dark:text-white/50">
|
||||
⌘{item.kbd}
|
||||
</kbd>
|
||||
)}
|
||||
</button>
|
||||
</MenuItem>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</MenuItems>
|
||||
)}
|
||||
</div>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
<Ariakit.MenuProvider store={menu}>
|
||||
{trigger}
|
||||
<Ariakit.Menu
|
||||
id={menuId}
|
||||
className="z-50 mt-2 overflow-hidden rounded-lg bg-header-primary p-1.5 shadow-lg outline-none focus-visible:ring-2 focus-visible:ring-ring-primary"
|
||||
gutter={8}
|
||||
>
|
||||
{items
|
||||
.filter((item) => item.show !== false)
|
||||
.map((item, index) =>
|
||||
item.separate === true ? (
|
||||
<Ariakit.MenuSeparator key={index} className="my-1 h-px bg-white/10" />
|
||||
) : (
|
||||
<Ariakit.MenuItem
|
||||
key={index}
|
||||
className="group flex w-full cursor-pointer items-center gap-2 rounded-lg p-2.5 text-sm text-text-primary outline-none transition-colors duration-200 hover:bg-surface-hover focus:bg-surface-hover"
|
||||
disabled={item.disabled}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
if (item.onClick) {
|
||||
item.onClick();
|
||||
}
|
||||
menu.hide();
|
||||
}}
|
||||
>
|
||||
{item.icon != null && (
|
||||
<span className="mr-2 h-5 w-5" aria-hidden="true">
|
||||
{item.icon}
|
||||
</span>
|
||||
)}
|
||||
{item.label}
|
||||
{item.kbd != null && (
|
||||
<kbd className="ml-auto hidden font-sans text-xs text-black/50 group-hover:inline group-focus:inline dark:text-white/50">
|
||||
⌘{item.kbd}
|
||||
</kbd>
|
||||
)}
|
||||
</Ariakit.MenuItem>
|
||||
),
|
||||
)}
|
||||
</Ariakit.Menu>
|
||||
</Ariakit.MenuProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue