mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-19 09:50:15 +01:00
💡 style: switched to Ariakit's tooltip (#3748)
* inital Tooltip implementation and test * style(tooltip): L/R sidePanel and Nav * style(tooltip): unarchive button; refactor: `useArchiveHandler` and `ArchiveButton` * style(tooltip): Delete button * refactor: remove unused className prop in DeleteButton component * style(tooltip): finish final tooltip and fix bookmark edit and delete button * refactor(ui): remove TooltipTest and DropDownMenu component and unused imports * style: update mobile UI * fix: sidePanel icon not showing * feat(AttachFile): add tooltip * fix(NavToggle): remove button without this button, kb users don't have to manually press 2 times to change the focus Also, tooltips with buttons focus don't trigger * fix: right side panel issue with double button * fix: merge issues * fix: sharedLink table issue * chore: update ariakit and framer-motion version * a11y: kb toggle for sidebar * feat: tooltip for some buttons
This commit is contained in:
parent
e293ff63f9
commit
4ef5ae6f71
37 changed files with 747 additions and 967 deletions
|
|
@ -60,7 +60,7 @@ function SelectDropDownPop({
|
|||
<button
|
||||
data-testid="select-dropdown-button"
|
||||
className={cn(
|
||||
'pointer-cursor relative flex flex-col rounded-md border border-black/10 bg-white py-2 pl-3 pr-10 text-left focus:ring-0 focus:ring-offset-0 dark:border-gray-700 dark:bg-gray-800 sm:text-sm',
|
||||
'pointer-cursor relative flex flex-col rounded-lg border border-black/10 bg-white py-2 pl-3 pr-10 text-left focus:ring-0 focus:ring-offset-0 dark:border-gray-700 dark:bg-gray-800 sm:text-sm',
|
||||
'hover:bg-gray-50 radix-state-open:bg-gray-50 dark:hover:bg-gray-700 dark:radix-state-open:bg-gray-700',
|
||||
)}
|
||||
aria-label={`Select ${title}`}
|
||||
|
|
|
|||
|
|
@ -1,46 +1,65 @@
|
|||
import * as React from 'react';
|
||||
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
||||
import { cn } from '~/utils';
|
||||
import * as Ariakit from '@ariakit/react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { forwardRef, useMemo } from 'react';
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root;
|
||||
interface TooltipAnchorProps extends Ariakit.TooltipAnchorProps {
|
||||
description: string;
|
||||
side?: 'top' | 'bottom' | 'left' | 'right';
|
||||
}
|
||||
|
||||
const TooltipTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Trigger>
|
||||
>((props, ref) => <TooltipPrimitive.Trigger ref={ref} {...props} />);
|
||||
TooltipTrigger.displayName = TooltipPrimitive.Trigger.displayName;
|
||||
export const TooltipAnchor = forwardRef<HTMLDivElement, TooltipAnchorProps>(function TooltipAnchor(
|
||||
{ description, side = 'top', role, ...props },
|
||||
ref,
|
||||
) {
|
||||
const tooltip = Ariakit.useTooltipStore({ placement: side });
|
||||
const mounted = Ariakit.useStoreState(tooltip, (state) => state.mounted);
|
||||
const placement = Ariakit.useStoreState(tooltip, (state) => state.placement);
|
||||
|
||||
const TooltipPortal = TooltipPrimitive.Portal;
|
||||
const { x, y } = useMemo(() => {
|
||||
const dir = placement.split('-')[0];
|
||||
switch (dir) {
|
||||
case 'top':
|
||||
return { x: 0, y: -8 };
|
||||
case 'bottom':
|
||||
return { x: 0, y: 8 };
|
||||
case 'left':
|
||||
return { x: -8, y: 0 };
|
||||
case 'right':
|
||||
return { x: 8, y: 0 };
|
||||
default:
|
||||
return { x: 0, y: 0 };
|
||||
}
|
||||
}, [placement]);
|
||||
|
||||
const TooltipArrow = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Arrow>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Arrow>
|
||||
>((props, ref) => <TooltipPrimitive.Arrow ref={ref} {...props} />);
|
||||
TooltipArrow.displayName = TooltipPrimitive.Arrow.displayName;
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (role === 'button' && event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
(event.target as HTMLDivElement).click();
|
||||
}
|
||||
};
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className = '', forceMount, children, ...props }, ref) => (
|
||||
<TooltipPortal forceMount={forceMount}>
|
||||
<TooltipPrimitive.Content
|
||||
className={cn(
|
||||
'shadow-xs relative z-[1000] max-w-xs rounded-lg border border-gray-900/10 bg-gray-900 p-1 transition-opacity',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
style={{ userSelect: 'none' }}
|
||||
>
|
||||
<span className="flex items-center whitespace-pre-wrap px-2 py-1 text-center text-sm font-medium normal-case text-white">
|
||||
{children}
|
||||
<TooltipArrow className="TooltipArrow" />
|
||||
</span>
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPortal>
|
||||
));
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipPortal, TooltipContent, TooltipArrow, TooltipProvider };
|
||||
return (
|
||||
<Ariakit.TooltipProvider store={tooltip} hideTimeout={0}>
|
||||
<Ariakit.TooltipAnchor {...props} ref={ref} role={role} onKeyDown={handleKeyDown} />
|
||||
<AnimatePresence>
|
||||
{mounted && (
|
||||
<Ariakit.Tooltip
|
||||
gutter={4}
|
||||
alwaysVisible
|
||||
className="tooltip"
|
||||
render={
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x, y }}
|
||||
animate={{ opacity: 1, x: 0, y: 0 }}
|
||||
exit={{ opacity: 0, x, y }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Ariakit.TooltipArrow />
|
||||
{description}
|
||||
</Ariakit.Tooltip>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</Ariakit.TooltipProvider>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,96 +0,0 @@
|
|||
import { ReactElement } from 'react';
|
||||
import {
|
||||
OGDialog,
|
||||
OGDialogTrigger,
|
||||
Label,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '~/components/ui';
|
||||
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||
import { CrossIcon } from '~/components/svg';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
export default function TooltipIcon({
|
||||
disabled,
|
||||
appendLabel = false,
|
||||
title,
|
||||
className = '',
|
||||
confirm,
|
||||
confirmMessage,
|
||||
icon,
|
||||
tabIndex,
|
||||
onFocus,
|
||||
onBlur,
|
||||
}: {
|
||||
disabled: boolean;
|
||||
title: string;
|
||||
appendLabel?: boolean;
|
||||
className?: string;
|
||||
confirm?: () => void;
|
||||
confirmMessage?: ReactElement;
|
||||
icon?: ReactElement;
|
||||
tabIndex?: number;
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
|
||||
const renderDeleteButton = () => {
|
||||
if (appendLabel) {
|
||||
return (
|
||||
<>
|
||||
{icon} {localize('com_ui_delete')}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<TooltipProvider delayDuration={250}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>{icon}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" sideOffset={0}>
|
||||
{localize('com_ui_delete')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
|
||||
if (!confirmMessage) {
|
||||
return (
|
||||
<button
|
||||
className={className}
|
||||
onClick={confirm}
|
||||
tabIndex={tabIndex}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
>
|
||||
{disabled ? <CrossIcon /> : renderDeleteButton()}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<OGDialog>
|
||||
<OGDialogTrigger asChild>
|
||||
<button className={className} tabIndex={tabIndex} onFocus={onFocus} onBlur={onBlur}>
|
||||
{disabled ? <CrossIcon /> : renderDeleteButton()}
|
||||
</button>
|
||||
</OGDialogTrigger>
|
||||
<OGDialogTemplate
|
||||
showCloseButton={false}
|
||||
title={title}
|
||||
className="max-w-[450px]"
|
||||
main={<Label className="text-left text-sm font-medium">{confirmMessage}</Label>}
|
||||
selection={{
|
||||
selectHandler: confirm,
|
||||
selectClasses:
|
||||
'bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 text-white',
|
||||
selectText: localize('com_ui_delete'),
|
||||
}}
|
||||
/>
|
||||
</OGDialog>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue