Fix spell check tooltip positioning to appear right above misspelled words

- Update SpellCheckTooltip styling to match commit 663c921 design
- Add numbered suggestions with hover highlighting and blue selection states
- Include keyboard hints and improved action buttons with icons
- Fix tooltip positioning to appear directly above words with minimal gap
- Remove translateY(-100%) transform that was pushing tooltip too far away
- Position tooltip 5px above word with only horizontal centering
- Maintain viewport boundary detection to prevent cutoff on edges
- Improve overall tooltip UX with professional styling and close proximity
This commit is contained in:
spencrmartin
2025-09-25 16:27:28 -04:00
committed by Alex Hancock
parent 1c5b585e6c
commit 485a6836e3
+87 -42
View File
@@ -25,6 +25,7 @@ const SpellCheckTooltip: React.FC<SpellCheckTooltipProps> = ({
}) => {
const tooltipRef = useRef<HTMLDivElement>(null);
const [adjustedPosition, setAdjustedPosition] = useState(position);
const [selectedIndex, setSelectedIndex] = useState(0);
// Adjust position to keep tooltip within viewport
useEffect(() => {
@@ -33,22 +34,32 @@ const SpellCheckTooltip: React.FC<SpellCheckTooltipProps> = ({
const rect = tooltip.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const tooltipWidth = rect.width || 200; // fallback width
const tooltipHeight = rect.height || 100; // fallback height
let adjustedX = position.x;
let adjustedY = position.y;
// Calculate the tooltip's left edge when centered
const tooltipLeftEdge = position.x - (tooltipWidth / 2);
const tooltipRightEdge = position.x + (tooltipWidth / 2);
// Adjust horizontal position if tooltip would overflow
if (position.x + rect.width > viewportWidth) {
adjustedX = viewportWidth - rect.width - 10;
}
if (adjustedX < 10) {
adjustedX = 10;
if (tooltipLeftEdge < 10) {
// If left edge would be cut off, align to left edge with padding
adjustedX = (tooltipWidth / 2) + 10;
} else if (tooltipRightEdge > viewportWidth - 10) {
// If right edge would be cut off, align to right edge with padding
adjustedX = viewportWidth - (tooltipWidth / 2) - 10;
}
// Adjust vertical position if tooltip would overflow
if (position.y + rect.height > viewportHeight) {
adjustedY = position.y - rect.height - 10;
if (position.y + tooltipHeight + 50 > viewportHeight) {
// Show above the word instead of below
adjustedY = position.y - tooltipHeight - 10;
}
// Ensure tooltip doesn't go above viewport
if (adjustedY < 10) {
adjustedY = 10;
}
@@ -59,65 +70,99 @@ const SpellCheckTooltip: React.FC<SpellCheckTooltipProps> = ({
if (!isVisible) return null;
console.log('🖱️ TOOLTIP COMPONENT: Rendering visible tooltip');
return (
<div
ref={tooltipRef}
data-spell-tooltip="true"
className="fixed z-[9999] bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg py-2 px-0 min-w-[200px] max-w-[300px]"
tabIndex={-1} // Make it focusable for keyboard events
data-spell-tooltip="true" // For click detection
className="fixed z-50 bg-background-default border border-border-default rounded-lg shadow-xl py-2 min-w-48 max-w-64 outline-none"
style={{
left: `${adjustedPosition.x}px`,
top: `${adjustedPosition.y + 25}px`, // Offset below the word
transform: 'translateX(-50%)', // Center horizontally
top: `${adjustedPosition.y - 5}px`, // Position right above the word
transform: 'translateX(-50%)', // Only center horizontally
boxShadow: '0 10px 25px rgba(0, 0, 0, 0.15), 0 4px 6px rgba(0, 0, 0, 0.1)',
}}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
{/* Header */}
<div className="px-3 pb-2 border-b border-gray-200 dark:border-gray-700">
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
Misspelled: <span className="text-red-600 dark:text-red-400">{misspelledWord}</span>
</div>
<div className="px-3 py-1 text-xs text-text-muted border-b border-border-subtle mb-1 font-medium">
Suggestions for "<span className="text-red-600 dark:text-red-400 font-semibold">{misspelledWord}</span>"
</div>
{/* Suggestions */}
{suggestions.length > 0 && (
<div className="py-1">
<div className="px-3 py-1 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">
Suggestions
</div>
{suggestions.length > 0 ? (
<div className="max-h-32 overflow-y-auto">
{suggestions.slice(0, 5).map((suggestion, index) => (
<button
key={index}
className="w-full px-3 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 focus:bg-gray-100 dark:focus:bg-gray-700 focus:outline-none transition-colors duration-150"
onClick={() => onSuggestionSelect(suggestion)}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
console.log('🖱️ SUGGESTION CLICKED:', suggestion);
onSuggestionSelect(suggestion);
}}
onMouseEnter={() => setSelectedIndex(index)}
className={`w-full text-left px-3 py-2 text-sm transition-all duration-150 flex items-center gap-2 ${
selectedIndex === index
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-900 dark:text-blue-100 border-l-2 border-blue-500'
: 'text-text-default hover:bg-background-subtle'
}`}
>
{suggestion}
<span
className={`w-5 h-5 flex items-center justify-center text-xs rounded text-[10px] font-bold ${
selectedIndex === index
? 'bg-blue-500 text-white'
: 'bg-text-muted text-white'
}`}
>
{index + 1}
</span>
<span className="font-medium truncate">{suggestion}</span>
</button>
))}
</div>
)}
{/* No suggestions message */}
{suggestions.length === 0 && (
<div className="px-3 py-2 text-sm text-gray-500 dark:text-gray-400">
) : (
<div className="px-3 py-2 text-sm text-text-muted italic">
No suggestions available
</div>
)}
{/* Actions */}
<div className="border-t border-gray-200 dark:border-gray-700 pt-1">
<button
className="w-full px-3 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 focus:bg-gray-100 dark:focus:bg-gray-700 focus:outline-none transition-colors duration-150"
onClick={onAddToDictionary}
>
Add to Dictionary
</button>
<button
className="w-full px-3 py-2 text-left text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 focus:bg-gray-100 dark:focus:bg-gray-700 focus:outline-none transition-colors duration-150"
onClick={onIgnore}
>
Ignore
</button>
{/* Separator */}
<div className="border-t border-border-subtle my-1" />
{/* Additional actions */}
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
console.log('🖱️ ADD TO DICTIONARY CLICKED');
onAddToDictionary();
}}
className="w-full text-left px-3 py-1.5 text-xs text-text-muted hover:bg-background-subtle transition-colors flex items-center gap-2"
>
<span className="text-green-600 dark:text-green-400">+</span>
Add to dictionary
</button>
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
console.log('🖱️ IGNORE CLICKED');
onIgnore();
}}
className="w-full text-left px-3 py-1.5 text-xs text-text-muted hover:bg-background-subtle transition-colors flex items-center gap-2"
>
<span className="text-text-muted">×</span>
Ignore word
</button>
{/* Keyboard hints */}
<div className="px-3 py-1 text-[10px] text-text-muted border-t border-border-subtle mt-1">
Press 1-5 to select • ↑↓ to navigate • Enter to apply • Esc to close
</div>
</div>
);