mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 08:50:15 +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,9 +1,10 @@
|
||||||
import { useState } from 'react';
|
import { useState, useId } from 'react';
|
||||||
import { Upload, Share2 } from 'lucide-react';
|
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import * as Ariakit from '@ariakit/react';
|
||||||
|
import { Upload, Share2 } from 'lucide-react';
|
||||||
import { ShareButton } from '~/components/Conversations/ConvoOptions';
|
import { ShareButton } from '~/components/Conversations/ConvoOptions';
|
||||||
import { Button, DropdownPopup } from '~/components/ui';
|
|
||||||
import { useMediaQuery, useLocalize } from '~/hooks';
|
import { useMediaQuery, useLocalize } from '~/hooks';
|
||||||
|
import { DropdownPopup } from '~/components/ui';
|
||||||
import { ExportModal } from '../Nav';
|
import { ExportModal } from '../Nav';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
|
|
@ -13,11 +14,13 @@ export default function ExportAndShareMenu({
|
||||||
isSharedButtonEnabled: boolean;
|
isSharedButtonEnabled: boolean;
|
||||||
}) {
|
}) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const conversation = useRecoilValue(store.conversationByIndex(0));
|
|
||||||
const [isPopoverActive, setIsPopoverActive] = useState(false);
|
|
||||||
const [showExports, setShowExports] = useState(false);
|
const [showExports, setShowExports] = useState(false);
|
||||||
|
const [isPopoverActive, setIsPopoverActive] = useState(false);
|
||||||
const [showShareDialog, setShowShareDialog] = useState(false);
|
const [showShareDialog, setShowShareDialog] = useState(false);
|
||||||
|
|
||||||
|
const menuId = useId();
|
||||||
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
||||||
|
const conversation = useRecoilValue(store.conversationByIndex(0));
|
||||||
|
|
||||||
const exportable =
|
const exportable =
|
||||||
conversation &&
|
conversation &&
|
||||||
|
|
@ -60,20 +63,19 @@ export default function ExportAndShareMenu({
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DropdownPopup
|
<DropdownPopup
|
||||||
|
menuId={menuId}
|
||||||
isOpen={isPopoverActive}
|
isOpen={isPopoverActive}
|
||||||
setIsOpen={setIsPopoverActive}
|
setIsOpen={setIsPopoverActive}
|
||||||
trigger={
|
trigger={
|
||||||
<Button
|
<Ariakit.MenuButton
|
||||||
id="export-menu-button"
|
id="export-menu-button"
|
||||||
aria-label="Export options"
|
aria-label="Export options"
|
||||||
variant="outline"
|
className="mr-4 inline-flex h-10 w-10 items-center justify-center rounded-md border border-border-light bg-transparent p-0 text-sm font-medium transition-all duration-300 ease-in-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
|
||||||
className="mr-4 h-10 w-10 p-0 transition-all duration-300 ease-in-out"
|
|
||||||
>
|
>
|
||||||
<Upload className="icon-md dark:text-gray-300" aria-hidden="true" focusable="false" />
|
<Upload className="icon-md dark:text-gray-300" aria-hidden="true" focusable="false" />
|
||||||
</Button>
|
</Ariakit.MenuButton>
|
||||||
}
|
}
|
||||||
items={dropdownItems}
|
items={dropdownItems}
|
||||||
anchor="bottom end"
|
|
||||||
className={isSmallScreen ? '' : 'absolute right-0 top-0 mt-2'}
|
className={isSmallScreen ? '' : 'absolute right-0 top-0 mt-2'}
|
||||||
/>
|
/>
|
||||||
{showShareDialog && conversation.conversationId != null && (
|
{showShareDialog && conversation.conversationId != null && (
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { useRecoilValue } from 'recoil';
|
|
||||||
import { useParams } from 'react-router-dom';
|
|
||||||
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { Check, X } from 'lucide-react';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
import { Constants } from 'librechat-data-provider';
|
import { Constants } from 'librechat-data-provider';
|
||||||
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
|
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
|
||||||
import { Check, X } from 'lucide-react';
|
|
||||||
import type { MouseEvent, FocusEvent, KeyboardEvent } from 'react';
|
import type { MouseEvent, FocusEvent, KeyboardEvent } from 'react';
|
||||||
|
import type { TConversation } from 'librechat-data-provider';
|
||||||
import { useConversations, useNavigateToConvo, useMediaQuery } from '~/hooks';
|
import { useConversations, useNavigateToConvo, useMediaQuery } from '~/hooks';
|
||||||
import { useUpdateConversationMutation } from '~/data-provider';
|
import { useUpdateConversationMutation } from '~/data-provider';
|
||||||
import EndpointIcon from '~/components/Endpoints/EndpointIcon';
|
import EndpointIcon from '~/components/Endpoints/EndpointIcon';
|
||||||
|
|
@ -17,7 +17,19 @@ import store from '~/store';
|
||||||
|
|
||||||
type KeyEvent = KeyboardEvent<HTMLInputElement>;
|
type KeyEvent = KeyboardEvent<HTMLInputElement>;
|
||||||
|
|
||||||
export default function Conversation({ conversation, retainView, toggleNav, isLatestConvo }) {
|
type ConversationProps = {
|
||||||
|
conversation: TConversation;
|
||||||
|
retainView: () => void;
|
||||||
|
toggleNav: () => void;
|
||||||
|
isLatestConvo: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Conversation({
|
||||||
|
conversation,
|
||||||
|
retainView,
|
||||||
|
toggleNav,
|
||||||
|
isLatestConvo,
|
||||||
|
}: ConversationProps) {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const currentConvoId = useMemo(() => params.conversationId, [params.conversationId]);
|
const currentConvoId = useMemo(() => params.conversationId, [params.conversationId]);
|
||||||
const updateConvoMutation = useUpdateConversationMutation(currentConvoId ?? '');
|
const updateConvoMutation = useUpdateConversationMutation(currentConvoId ?? '');
|
||||||
|
|
@ -33,7 +45,7 @@ export default function Conversation({ conversation, retainView, toggleNav, isLa
|
||||||
const [isPopoverActive, setIsPopoverActive] = useState(false);
|
const [isPopoverActive, setIsPopoverActive] = useState(false);
|
||||||
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
||||||
|
|
||||||
const clickHandler = async (event: React.MouseEvent<HTMLAnchorElement>) => {
|
const clickHandler = async (event: MouseEvent<HTMLAnchorElement>) => {
|
||||||
if (event.button === 0 && (event.ctrlKey || event.metaKey)) {
|
if (event.button === 0 && (event.ctrlKey || event.metaKey)) {
|
||||||
toggleNav();
|
toggleNav();
|
||||||
return;
|
return;
|
||||||
|
|
@ -47,12 +59,17 @@ export default function Conversation({ conversation, retainView, toggleNav, isLa
|
||||||
toggleNav();
|
toggleNav();
|
||||||
|
|
||||||
// set document title
|
// set document title
|
||||||
|
if (typeof title === 'string' && title.length > 0) {
|
||||||
document.title = title;
|
document.title = title;
|
||||||
|
}
|
||||||
/* Note: Latest Message should not be reset if existing convo */
|
/* Note: Latest Message should not be reset if existing convo */
|
||||||
navigateWithLastTools(conversation, !conversationId || conversationId === Constants.NEW_CONVO);
|
navigateWithLastTools(
|
||||||
|
conversation,
|
||||||
|
!(conversationId ?? '') || conversationId === Constants.NEW_CONVO,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renameHandler = (e: MouseEvent<HTMLButtonElement>) => {
|
const renameHandler: (e: MouseEvent<HTMLButtonElement>) => void = () => {
|
||||||
setIsPopoverActive(false);
|
setIsPopoverActive(false);
|
||||||
setTitleInput(title);
|
setTitleInput(title);
|
||||||
setRenaming(true);
|
setRenaming(true);
|
||||||
|
|
@ -70,8 +87,12 @@ export default function Conversation({ conversation, retainView, toggleNav, isLa
|
||||||
if (titleInput === title) {
|
if (titleInput === title) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (typeof conversationId !== 'string' || conversationId === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
updateConvoMutation.mutate(
|
updateConvoMutation.mutate(
|
||||||
{ conversationId, title: titleInput },
|
{ conversationId, title: titleInput ?? '' },
|
||||||
{
|
{
|
||||||
onSuccess: () => refreshConversations(),
|
onSuccess: () => refreshConversations(),
|
||||||
onError: () => {
|
onError: () => {
|
||||||
|
|
@ -101,14 +122,17 @@ export default function Conversation({ conversation, retainView, toggleNav, isLa
|
||||||
setRenaming(false);
|
setRenaming(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isActiveConvo =
|
const isActiveConvo: boolean =
|
||||||
currentConvoId === conversationId ||
|
currentConvoId === conversationId ||
|
||||||
(isLatestConvo && currentConvoId === 'new' && activeConvos[0] && activeConvos[0] !== 'new');
|
(isLatestConvo &&
|
||||||
|
currentConvoId === 'new' &&
|
||||||
|
activeConvos[0] != null &&
|
||||||
|
activeConvos[0] !== 'new');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'group relative mt-2 flex h-9 items-center rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700',
|
'group relative mt-2 flex h-9 w-full items-center rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700',
|
||||||
isActiveConvo ? 'bg-gray-200 dark:bg-gray-700' : '',
|
isActiveConvo ? 'bg-gray-200 dark:bg-gray-700' : '',
|
||||||
isSmallScreen ? 'h-12' : '',
|
isSmallScreen ? 'h-12' : '',
|
||||||
)}
|
)}
|
||||||
|
|
@ -119,7 +143,7 @@ export default function Conversation({ conversation, retainView, toggleNav, isLa
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
type="text"
|
type="text"
|
||||||
className="w-full rounded bg-transparent p-0.5 text-sm leading-tight outline-none"
|
className="w-full rounded bg-transparent p-0.5 text-sm leading-tight outline-none"
|
||||||
value={titleInput}
|
value={titleInput ?? ''}
|
||||||
onChange={(e) => setTitleInput(e.target.value)}
|
onChange={(e) => setTitleInput(e.target.value)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
/>
|
/>
|
||||||
|
|
@ -141,7 +165,7 @@ export default function Conversation({ conversation, retainView, toggleNav, isLa
|
||||||
'flex grow cursor-pointer items-center gap-2 overflow-hidden whitespace-nowrap break-all rounded-lg px-2 py-2',
|
'flex grow cursor-pointer items-center gap-2 overflow-hidden whitespace-nowrap break-all rounded-lg px-2 py-2',
|
||||||
isActiveConvo ? 'bg-gray-200 dark:bg-gray-700' : '',
|
isActiveConvo ? 'bg-gray-200 dark:bg-gray-700' : '',
|
||||||
)}
|
)}
|
||||||
title={title}
|
title={title ?? ''}
|
||||||
>
|
>
|
||||||
<EndpointIcon
|
<EndpointIcon
|
||||||
conversation={conversation}
|
conversation={conversation}
|
||||||
|
|
@ -149,16 +173,9 @@ export default function Conversation({ conversation, retainView, toggleNav, isLa
|
||||||
size={20}
|
size={20}
|
||||||
context="menu-item"
|
context="menu-item"
|
||||||
/>
|
/>
|
||||||
{!renaming && (
|
|
||||||
<div className="relative line-clamp-1 flex-1 grow overflow-hidden">{title}</div>
|
<div className="relative line-clamp-1 flex-1 grow overflow-hidden">{title}</div>
|
||||||
)}
|
|
||||||
{isActiveConvo ? (
|
{isActiveConvo ? (
|
||||||
<div
|
<div className="absolute bottom-0 right-0 top-0 w-20 rounded-r-lg bg-gradient-to-l" />
|
||||||
className={cn(
|
|
||||||
'absolute bottom-0 right-0 top-0 w-20 rounded-r-lg bg-gradient-to-l',
|
|
||||||
!renaming ? 'from-gray-200 from-40% to-transparent dark:from-gray-700' : '',
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="absolute bottom-0 right-0 top-0 w-20 rounded-r-lg bg-gradient-to-l from-gray-50 from-0% to-transparent group-hover:from-gray-200 group-hover:from-40% dark:from-gray-850 dark:group-hover:from-gray-700" />
|
<div className="absolute bottom-0 right-0 top-0 w-20 rounded-r-lg bg-gradient-to-l from-gray-50 from-0% to-transparent group-hover:from-gray-200 group-hover:from-40% dark:from-gray-850 dark:group-hover:from-gray-700" />
|
||||||
)}
|
)}
|
||||||
|
|
@ -167,7 +184,9 @@ export default function Conversation({ conversation, retainView, toggleNav, isLa
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'mr-2',
|
'mr-2',
|
||||||
isPopoverActive || isActiveConvo ? 'flex' : 'hidden group-hover:flex',
|
isPopoverActive || isActiveConvo
|
||||||
|
? 'flex'
|
||||||
|
: 'hidden group-focus-within:flex group-hover:flex',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ConvoOptions
|
<ConvoOptions
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import { useState } from 'react';
|
import { useState, useId } from 'react';
|
||||||
|
import * as Ariakit from '@ariakit/react';
|
||||||
import { Ellipsis, Share2, Archive, Pen, Trash } from 'lucide-react';
|
import { Ellipsis, Share2, Archive, Pen, Trash } from 'lucide-react';
|
||||||
import { useGetStartupConfig } from 'librechat-data-provider/react-query';
|
import { useGetStartupConfig } from 'librechat-data-provider/react-query';
|
||||||
import { Button } from '~/components/ui';
|
|
||||||
import { useArchiveHandler } from './ArchiveButton';
|
import { useArchiveHandler } from './ArchiveButton';
|
||||||
import { DropdownPopup } from '~/components/ui';
|
import { DropdownPopup } from '~/components/ui';
|
||||||
import DeleteButton from './DeleteButton';
|
import DeleteButton from './DeleteButton';
|
||||||
import ShareButton from './ShareButton';
|
import ShareButton from './ShareButton';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
|
import { cn } from '~/utils';
|
||||||
|
|
||||||
export default function ConvoOptions({
|
export default function ConvoOptions({
|
||||||
conversation,
|
conversation,
|
||||||
|
|
@ -57,27 +58,29 @@ export default function ConvoOptions({
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const menuId = useId();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DropdownPopup
|
<DropdownPopup
|
||||||
isOpen={isPopoverActive}
|
isOpen={isPopoverActive}
|
||||||
setIsOpen={setIsPopoverActive}
|
setIsOpen={setIsPopoverActive}
|
||||||
trigger={
|
trigger={
|
||||||
<Button
|
<Ariakit.MenuButton
|
||||||
id="conversation-menu-button"
|
id="conversation-menu-button"
|
||||||
aria-label="conversation-menu-button"
|
aria-label={localize('com_nav_convo_menu_options')}
|
||||||
variant="link"
|
className={cn(
|
||||||
className="z-10 h-7 w-7 border-none p-0 transition-all duration-200 ease-in-out"
|
'z-30 inline-flex h-7 w-7 items-center justify-center gap-2 rounded-md border-none p-0 text-sm font-medium 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',
|
||||||
>
|
|
||||||
<Ellipsis className="icon-md text-text-secondary" />
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
items={dropdownItems}
|
|
||||||
className={`${
|
|
||||||
isActiveConvo === true
|
isActiveConvo === true
|
||||||
? 'opacity-100'
|
? 'opacity-100'
|
||||||
: 'opacity-0 focus:opacity-100 group-hover:opacity-100 data-[open]:opacity-100'
|
: 'opacity-0 focus:opacity-100 group-focus-within:opacity-100 group-hover:opacity-100 data-[open]:opacity-100',
|
||||||
}`}
|
)}
|
||||||
|
>
|
||||||
|
<Ellipsis className="icon-md text-text-secondary" />
|
||||||
|
</Ariakit.MenuButton>
|
||||||
|
}
|
||||||
|
items={dropdownItems}
|
||||||
|
menuId={menuId}
|
||||||
/>
|
/>
|
||||||
{showShareDialog && (
|
{showShareDialog && (
|
||||||
<ShareButton
|
<ShareButton
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ const NewChatButtonIcon = ({ conversation }: { conversation: TConversation | nul
|
||||||
<ConvoIconURL preset={conversation} endpointIconURL={iconURL} context="nav" />
|
<ConvoIconURL preset={conversation} endpointIconURL={iconURL} context="nav" />
|
||||||
) : (
|
) : (
|
||||||
<div className="shadow-stroke relative flex h-full items-center justify-center rounded-full bg-white text-black">
|
<div className="shadow-stroke relative flex h-full items-center justify-center rounded-full bg-white text-black">
|
||||||
{endpoint && Icon && (
|
{endpoint && Icon != null && (
|
||||||
<Icon
|
<Icon
|
||||||
size={41}
|
size={41}
|
||||||
context="nav"
|
context="nav"
|
||||||
|
|
@ -82,7 +82,7 @@ export default function NewChat({
|
||||||
return (
|
return (
|
||||||
<TooltipProvider delayDuration={250}>
|
<TooltipProvider delayDuration={250}>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<div className="sticky left-0 right-0 top-0 z-20 bg-surface-primary-alt pt-3.5">
|
<div className="sticky left-0 right-0 top-0 z-50 bg-surface-primary-alt pt-3.5">
|
||||||
<div className="pb-0.5 last:pb-0" style={{ transform: 'none' }}>
|
<div className="pb-0.5 last:pb-0" style={{ transform: 'none' }}>
|
||||||
<a
|
<a
|
||||||
href="/"
|
href="/"
|
||||||
|
|
@ -93,7 +93,7 @@ export default function NewChat({
|
||||||
aria-label={localize('com_ui_new_chat')}
|
aria-label={localize('com_ui_new_chat')}
|
||||||
>
|
>
|
||||||
<NewChatButtonIcon conversation={conversation} />
|
<NewChatButtonIcon conversation={conversation} />
|
||||||
<div className="text-token-text-primary grow overflow-hidden text-ellipsis whitespace-nowrap text-sm">
|
<div className="grow overflow-hidden text-ellipsis whitespace-nowrap text-sm text-text-primary">
|
||||||
{localize('com_ui_new_chat')}
|
{localize('com_ui_new_chat')}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
|
|
@ -102,7 +102,7 @@ export default function NewChat({
|
||||||
<button
|
<button
|
||||||
id="nav-new-chat-btn"
|
id="nav-new-chat-btn"
|
||||||
aria-label="nav-new-chat-btn"
|
aria-label="nav-new-chat-btn"
|
||||||
className="text-token-text-primary"
|
className="text-text-primary"
|
||||||
>
|
>
|
||||||
<NewChatIcon className="size-5" />
|
<NewChatIcon className="size-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react';
|
import * as Ariakit from '@ariakit/react';
|
||||||
|
|
||||||
interface DropdownProps {
|
interface DropdownProps {
|
||||||
trigger: React.ReactNode;
|
trigger: React.ReactNode;
|
||||||
|
|
@ -15,89 +15,55 @@ interface DropdownProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
setIsOpen: (isOpen: boolean) => void;
|
setIsOpen: (isOpen: boolean) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
anchor?: string;
|
anchor?: { x: string; y: string };
|
||||||
|
menuId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DropdownPopup: React.FC<DropdownProps> = ({
|
const DropdownPopup: React.FC<DropdownProps> = ({ trigger, items, isOpen, setIsOpen, menuId }) => {
|
||||||
trigger,
|
const menu = Ariakit.useMenuStore({ open: isOpen, setOpen: setIsOpen });
|
||||||
items,
|
|
||||||
isOpen,
|
|
||||||
setIsOpen,
|
|
||||||
className,
|
|
||||||
anchor = { x: 'bottom', y: 'start' },
|
|
||||||
}) => {
|
|
||||||
const handleButtonClick = () => {
|
|
||||||
setIsOpen(!isOpen);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu>
|
<Ariakit.MenuProvider store={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}
|
{trigger}
|
||||||
</MenuButton>
|
<Ariakit.Menu
|
||||||
<Transition
|
id={menuId}
|
||||||
show={open}
|
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"
|
||||||
enter="transition-opacity duration-150"
|
gutter={8}
|
||||||
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
|
{items
|
||||||
.filter((item) => item.show !== false)
|
.filter((item) => item.show !== false)
|
||||||
.map((item, index) =>
|
.map((item, index) =>
|
||||||
item.separate ? (
|
item.separate === true ? (
|
||||||
<div key={index} className="my-1 h-px bg-white/10" />
|
<Ariakit.MenuSeparator key={index} className="my-1 h-px bg-white/10" />
|
||||||
) : (
|
) : (
|
||||||
<MenuItem key={index}>
|
<Ariakit.MenuItem
|
||||||
<button
|
key={index}
|
||||||
onClick={item.onClick}
|
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"
|
||||||
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}
|
disabled={item.disabled}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (item.onClick) {
|
||||||
|
item.onClick();
|
||||||
|
}
|
||||||
|
menu.hide();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{item.icon && (
|
{item.icon != null && (
|
||||||
<span className="mr-2 h-5 w-5" aria-hidden="true">
|
<span className="mr-2 h-5 w-5" aria-hidden="true">
|
||||||
{item.icon}
|
{item.icon}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{item.label}
|
{item.label}
|
||||||
{item.kbd && (
|
{item.kbd != null && (
|
||||||
<kbd className="ml-auto hidden font-sans text-xs text-black/50 group-data-[focus]:inline dark:text-white/50">
|
<kbd className="ml-auto hidden font-sans text-xs text-black/50 group-hover:inline group-focus:inline dark:text-white/50">
|
||||||
⌘{item.kbd}
|
⌘{item.kbd}
|
||||||
</kbd>
|
</kbd>
|
||||||
)}
|
)}
|
||||||
</button>
|
</Ariakit.MenuItem>
|
||||||
</MenuItem>
|
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
</div>
|
</Ariakit.Menu>
|
||||||
</MenuItems>
|
</Ariakit.MenuProvider>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Menu>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
// file deepcode ignore HardcodedNonCryptoSecret: No hardcoded secrets present in this file
|
// file deepcode ignore HardcodedNonCryptoSecret: No hardcoded secrets present in this file
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
com_nav_convo_menu_options: 'Conversation Menu Options',
|
||||||
com_ui_artifacts: 'Artifacts',
|
com_ui_artifacts: 'Artifacts',
|
||||||
com_ui_artifacts_toggle: 'Toggle Artifacts UI',
|
com_ui_artifacts_toggle: 'Toggle Artifacts UI',
|
||||||
com_nav_info_code_artifacts:
|
com_nav_info_code_artifacts:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue