Remediated test #1 results
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useRef, useEffect, ReactNode } from 'react';
|
||||
import React, { useState, useRef, useEffect, useLayoutEffect, useCallback, ReactNode } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Info, X } from 'lucide-react';
|
||||
|
||||
export interface HelpTooltipProps {
|
||||
@@ -25,14 +26,118 @@ export const HelpTooltip: React.FC<HelpTooltipProps> = ({
|
||||
ariaLabel = 'Information and guidance',
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [coords, setCoords] = useState<{ top: number; left: number } | null>(null);
|
||||
|
||||
// Close on Escape or click outside
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const updatePosition = useCallback(() => {
|
||||
if (!buttonRef.current) return;
|
||||
const triggerRect = buttonRef.current.getBoundingClientRect();
|
||||
const tooltipEl = tooltipRef.current;
|
||||
const tooltipWidth = tooltipEl ? tooltipEl.offsetWidth : 288;
|
||||
const tooltipHeight = tooltipEl ? tooltipEl.offsetHeight : 120;
|
||||
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
const PADDING = 12;
|
||||
const GAP = 8;
|
||||
|
||||
const spaceAbove = triggerRect.top - GAP - PADDING;
|
||||
const spaceBelow = viewportHeight - triggerRect.bottom - GAP - PADDING;
|
||||
const spaceLeft = triggerRect.left - GAP - PADDING;
|
||||
const spaceRight = viewportWidth - triggerRect.right - GAP - PADDING;
|
||||
|
||||
let chosenPlacement = placement;
|
||||
|
||||
if (placement === 'bottom') {
|
||||
if (spaceBelow < tooltipHeight && spaceAbove > spaceBelow) {
|
||||
chosenPlacement = 'top';
|
||||
}
|
||||
} else if (placement === 'top') {
|
||||
if (spaceAbove < tooltipHeight && spaceBelow > spaceAbove) {
|
||||
chosenPlacement = 'bottom';
|
||||
}
|
||||
} else if (placement === 'left') {
|
||||
if (spaceLeft < tooltipWidth && spaceRight > spaceLeft) {
|
||||
chosenPlacement = 'right';
|
||||
}
|
||||
} else if (placement === 'right') {
|
||||
if (spaceRight < tooltipWidth && spaceLeft > spaceRight) {
|
||||
chosenPlacement = 'left';
|
||||
}
|
||||
}
|
||||
|
||||
let top = 0;
|
||||
let left = 0;
|
||||
|
||||
if (chosenPlacement === 'top') {
|
||||
top = triggerRect.top - tooltipHeight - GAP;
|
||||
left = triggerRect.left + triggerRect.width / 2 - tooltipWidth / 2;
|
||||
} else if (chosenPlacement === 'bottom') {
|
||||
top = triggerRect.bottom + GAP;
|
||||
left = triggerRect.left + triggerRect.width / 2 - tooltipWidth / 2;
|
||||
} else if (chosenPlacement === 'left') {
|
||||
top = triggerRect.top + triggerRect.height / 2 - tooltipHeight / 2;
|
||||
left = triggerRect.left - tooltipWidth - GAP;
|
||||
} else {
|
||||
// right
|
||||
top = triggerRect.top + triggerRect.height / 2 - tooltipHeight / 2;
|
||||
left = triggerRect.right + GAP;
|
||||
}
|
||||
|
||||
// Viewport boundary clamping to guarantee tooltip is never cropped by screen edges
|
||||
const maxLeft = Math.max(PADDING, viewportWidth - tooltipWidth - PADDING);
|
||||
left = Math.max(PADDING, Math.min(left, maxLeft));
|
||||
|
||||
const maxTop = Math.max(PADDING, viewportHeight - tooltipHeight - PADDING);
|
||||
top = Math.max(PADDING, Math.min(top, maxTop));
|
||||
|
||||
setCoords({ top, left });
|
||||
}, [placement]);
|
||||
|
||||
// Position updates on open, resize, or scroll of any container
|
||||
useLayoutEffect(() => {
|
||||
if (!isOpen) {
|
||||
setCoords(null);
|
||||
return;
|
||||
}
|
||||
|
||||
updatePosition();
|
||||
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
if (typeof ResizeObserver !== 'undefined' && tooltipRef.current) {
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
updatePosition();
|
||||
});
|
||||
resizeObserver.observe(tooltipRef.current);
|
||||
}
|
||||
|
||||
window.addEventListener('resize', updatePosition, { passive: true });
|
||||
window.addEventListener('scroll', updatePosition, { capture: true, passive: true });
|
||||
|
||||
return () => {
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect();
|
||||
}
|
||||
window.removeEventListener('resize', updatePosition);
|
||||
window.removeEventListener('scroll', updatePosition, true);
|
||||
};
|
||||
}, [isOpen, updatePosition]);
|
||||
|
||||
// Close on Escape or click outside both button and tooltip
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
const handleOutsideClick = (e: MouseEvent | TouchEvent) => {
|
||||
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
||||
const target = e.target as Node;
|
||||
if (
|
||||
buttonRef.current &&
|
||||
!buttonRef.current.contains(target) &&
|
||||
tooltipRef.current &&
|
||||
!tooltipRef.current.contains(target)
|
||||
) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
@@ -66,16 +171,53 @@ export const HelpTooltip: React.FC<HelpTooltipProps> = ({
|
||||
lg: 'w-4 h-4',
|
||||
}[size];
|
||||
|
||||
const placementClasses = {
|
||||
top: 'bottom-full left-1/2 -translate-x-1/2 mb-2',
|
||||
bottom: 'top-full left-1/2 -translate-x-1/2 mt-2',
|
||||
left: 'right-full top-1/2 -translate-y-1/2 mr-2',
|
||||
right: 'left-full top-1/2 -translate-y-1/2 ml-2',
|
||||
}[placement];
|
||||
const tooltipPortal =
|
||||
isOpen && typeof document !== 'undefined'
|
||||
? createPortal(
|
||||
<div
|
||||
ref={tooltipRef}
|
||||
role="tooltip"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: coords ? `${coords.top}px` : '-9999px',
|
||||
left: coords ? `${coords.left}px` : '-9999px',
|
||||
visibility: coords ? 'visible' : 'hidden',
|
||||
}}
|
||||
className="z-[9999] w-72 max-w-[calc(100vw-24px)] p-3.5 rounded-xl bg-[#0c121e] border border-blue-500/30 text-slate-200 shadow-2xl backdrop-blur-md animate-in fade-in zoom-in-95 duration-150"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2 border-b border-slate-800/80 pb-1.5 mb-2">
|
||||
<div className="flex items-center gap-1.5 text-blue-400">
|
||||
<Info className="w-3.5 h-3.5 shrink-0" />
|
||||
<span className="font-semibold text-xs tracking-tight text-white">
|
||||
{title || 'Guidance & Information'}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
className="rounded-md p-0.5 text-slate-400 hover:text-white hover:bg-slate-800 transition cursor-pointer"
|
||||
aria-label="Close tooltip"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="text-[11px] leading-relaxed text-slate-300 font-normal space-y-1">
|
||||
{content}
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={`relative inline-flex items-center align-middle ${className}`}>
|
||||
<button
|
||||
ref={buttonRef}
|
||||
type="button"
|
||||
aria-label={ariaLabel}
|
||||
aria-expanded={isOpen}
|
||||
@@ -89,37 +231,7 @@ export const HelpTooltip: React.FC<HelpTooltipProps> = ({
|
||||
<Info className={`${iconSizes} shrink-0`} />
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div
|
||||
role="tooltip"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className={`absolute ${placementClasses} z-50 w-72 max-w-[calc(100vw-32px)] p-3 rounded-xl bg-[#0c121e] border border-blue-500/30 text-slate-200 shadow-2xl backdrop-blur-md animate-in fade-in zoom-in-95 duration-150`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2 border-b border-slate-800/80 pb-1.5 mb-2">
|
||||
<div className="flex items-center gap-1.5 text-blue-400">
|
||||
<Info className="w-3.5 h-3.5 shrink-0" />
|
||||
<span className="font-semibold text-xs tracking-tight text-white">
|
||||
{title || 'Guidance & Information'}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
className="rounded-md p-0.5 text-slate-400 hover:text-white hover:bg-slate-800 transition cursor-pointer"
|
||||
aria-label="Close tooltip"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="text-[11px] leading-relaxed text-slate-300 font-normal space-y-1">
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{tooltipPortal}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -43,24 +43,28 @@ export const ProblemDetailsAlert: React.FC<ProblemDetailsAlertProps> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Single error or fallback message displayed directly */}
|
||||
{(!parsed.errorsList || parsed.errorsList.length <= 1) && parsed.message && (
|
||||
{/* Direct error message or error list from ProblemDetails */}
|
||||
{parsed.errorsList && parsed.errorsList.length > 0 ? (
|
||||
<div className="mt-2 space-y-1.5">
|
||||
{parsed.message && !parsed.errorsList.includes(parsed.message) && (
|
||||
<p className="text-xs text-rose-200/90 leading-relaxed whitespace-pre-line">
|
||||
{parsed.message}
|
||||
</p>
|
||||
)}
|
||||
<ul className="space-y-1 bg-black/30 rounded-lg p-2.5 border border-rose-900/40">
|
||||
{parsed.errorsList.map((err, idx) => (
|
||||
<li key={idx} className="text-xs text-rose-200/90 flex items-start gap-2">
|
||||
<span className="text-rose-400 mt-0.5 font-bold">•</span>
|
||||
<span className="leading-snug">{err}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : parsed.message ? (
|
||||
<p className="text-xs mt-1.5 text-rose-200/90 leading-relaxed whitespace-pre-line">
|
||||
{parsed.message}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Multiple errors from ProblemDetails displayed directly */}
|
||||
{parsed.errorsList && parsed.errorsList.length > 1 && (
|
||||
<ul className="mt-2 space-y-1 bg-black/30 rounded-lg p-2.5 border border-rose-900/40">
|
||||
{parsed.errorsList.map((err, idx) => (
|
||||
<li key={idx} className="text-xs text-rose-200/90 flex items-start gap-2">
|
||||
<span className="text-rose-400 mt-0.5">•</span>
|
||||
<span className="leading-snug">{err}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
{parsed.technicalDetails && (
|
||||
<p className="text-[10px] font-mono text-slate-500 mt-2">
|
||||
|
||||
@@ -155,16 +155,21 @@ const ToastCard: React.FC<{
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Display errors directly if multiple, or single message directly */}
|
||||
{toast.errorsList && toast.errorsList.length > 1 ? (
|
||||
<ul className="mt-1.5 space-y-1">
|
||||
{toast.errorsList.map((err, idx) => (
|
||||
<li key={idx} className="text-xs text-rose-200/90 flex items-start gap-1.5">
|
||||
<span className="text-rose-400 mt-0.5">•</span>
|
||||
<span className="leading-snug break-words">{err}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{/* Display errors from ProblemDetails errors member */}
|
||||
{toast.errorsList && toast.errorsList.length > 0 ? (
|
||||
<div className="mt-1.5 space-y-1">
|
||||
{toast.message && !toast.errorsList.includes(toast.message) && (
|
||||
<p className="text-xs text-slate-300 leading-relaxed">{toast.message}</p>
|
||||
)}
|
||||
<ul className="space-y-1">
|
||||
{toast.errorsList.map((err, idx) => (
|
||||
<li key={idx} className="text-xs text-rose-200/90 flex items-start gap-1.5">
|
||||
<span className="text-rose-400 mt-0.5 font-bold">•</span>
|
||||
<span className="leading-snug break-words">{err}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : toast.message ? (
|
||||
<p className="text-xs mt-1.5 text-slate-300 leading-relaxed whitespace-pre-line break-words">
|
||||
{toast.message}
|
||||
|
||||
Reference in New Issue
Block a user