mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
💬 style: Enhance Tooltip with HTML support and Improve Styling (#8915)
* ✨ feat: Enhance Tooltip component with HTML support and styling improvements * ✨ feat: Integrate DOMPurify for HTML sanitization in Tooltip component
This commit is contained in:
parent
8238fb49e0
commit
e6fa01d514
5 changed files with 71 additions and 54 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@librechat/client",
|
||||
"version": "0.2.3",
|
||||
"version": "0.2.4",
|
||||
"description": "React components for LibreChat",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.es.js",
|
||||
|
|
@ -54,6 +54,7 @@
|
|||
"@react-spring/web": "^10.0.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"dompurify": "^3.2.6",
|
||||
"framer-motion": "^12.23.6",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"input-otp": "^1.4.2",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,16 @@
|
|||
line-height: 1.5rem;
|
||||
color: black;
|
||||
box-shadow: 0 2px 4px 0 rgb(0 0 0 / 0.25);
|
||||
/* Enhanced layout for longer descriptions */
|
||||
max-width: 320px;
|
||||
word-wrap: break-word;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.tooltip {
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip:where(.dark, .dark *) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import DOMPurify from 'dompurify';
|
||||
import * as Ariakit from '@ariakit/react';
|
||||
import { forwardRef, useId, useMemo } from 'react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { forwardRef, useMemo } from 'react';
|
||||
import { cn } from '~/utils';
|
||||
import './Tooltip.css';
|
||||
|
||||
|
|
@ -8,18 +9,47 @@ interface TooltipAnchorProps extends Ariakit.TooltipAnchorProps {
|
|||
description: string;
|
||||
side?: 'top' | 'bottom' | 'left' | 'right';
|
||||
className?: string;
|
||||
focusable?: boolean;
|
||||
role?: string;
|
||||
enableHTML?: boolean;
|
||||
}
|
||||
|
||||
export const TooltipAnchor = forwardRef<HTMLDivElement, TooltipAnchorProps>(function TooltipAnchor(
|
||||
{ description, side = 'top', className, role, ...props },
|
||||
{ description, side = 'top', className, role, enableHTML = false, ...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 id = useId();
|
||||
const sanitizer = useMemo(() => {
|
||||
const instance = DOMPurify();
|
||||
instance.addHook('afterSanitizeAttributes', (node) => {
|
||||
if (node.tagName && node.tagName === 'A') {
|
||||
node.setAttribute('target', '_blank');
|
||||
node.setAttribute('rel', 'noopener noreferrer');
|
||||
}
|
||||
});
|
||||
return instance;
|
||||
}, []);
|
||||
|
||||
const sanitizedHTML = useMemo(() => {
|
||||
if (!enableHTML) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
return sanitizer.sanitize(description, {
|
||||
ALLOWED_TAGS: ['a', 'strong', 'b', 'em', 'i', 'br', 'code'],
|
||||
ALLOWED_ATTR: ['href', 'class', 'target', 'rel'],
|
||||
ALLOW_DATA_ATTR: false,
|
||||
ALLOW_ARIA_ATTR: false,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Sanitization failed', error);
|
||||
return description;
|
||||
}
|
||||
}, [enableHTML, description, sanitizer]);
|
||||
|
||||
const { x, y } = useMemo(() => {
|
||||
const dir = placement.split('-')[0];
|
||||
switch (dir) {
|
||||
|
|
@ -49,6 +79,7 @@ export const TooltipAnchor = forwardRef<HTMLDivElement, TooltipAnchorProps>(func
|
|||
{...props}
|
||||
ref={ref}
|
||||
role={role}
|
||||
aria-describedby={id}
|
||||
onKeyDown={handleKeyDown}
|
||||
className={cn('cursor-pointer', className)}
|
||||
/>
|
||||
|
|
@ -58,6 +89,7 @@ export const TooltipAnchor = forwardRef<HTMLDivElement, TooltipAnchorProps>(func
|
|||
gutter={4}
|
||||
alwaysVisible
|
||||
className="tooltip"
|
||||
id={id}
|
||||
render={
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x, y }}
|
||||
|
|
@ -67,7 +99,15 @@ export const TooltipAnchor = forwardRef<HTMLDivElement, TooltipAnchorProps>(func
|
|||
}
|
||||
>
|
||||
<Ariakit.TooltipArrow />
|
||||
{description}
|
||||
{enableHTML ? (
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: sanitizedHTML,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
description
|
||||
)}
|
||||
</Ariakit.Tooltip>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue