👐 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:
Danny Avila 2024-08-30 13:39:30 -04:00 committed by GitHub
parent 2ce4f66218
commit 0a359aa705
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 122 additions and 131 deletions

View file

@ -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>
);
};