mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 11:23:10 +02:00
Feature/v2/tooltips (#4112)
# Description of Changes - added tooltips to ocr and compress - added the tooltip component which can be used either directly, or through the toolstep component --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details.
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { clamp } from '../utils/genericUtils';
|
||||
import { getSidebarInfo } from '../utils/sidebarUtils';
|
||||
import { SidebarRefs, SidebarState } from '../types/sidebar';
|
||||
|
||||
type Position = 'right' | 'left' | 'top' | 'bottom';
|
||||
|
||||
interface PlacementResult {
|
||||
top: number;
|
||||
left: number;
|
||||
}
|
||||
|
||||
interface PositionState {
|
||||
coords: { top: number; left: number; arrowOffset: number | null };
|
||||
positionReady: boolean;
|
||||
}
|
||||
|
||||
function place(
|
||||
triggerRect: DOMRect,
|
||||
tooltipRect: DOMRect,
|
||||
position: Position,
|
||||
offset: number
|
||||
): PlacementResult {
|
||||
let top = 0;
|
||||
let left = 0;
|
||||
|
||||
switch (position) {
|
||||
case 'right':
|
||||
top = triggerRect.top + triggerRect.height / 2 - tooltipRect.height / 2;
|
||||
left = triggerRect.right + offset;
|
||||
break;
|
||||
case 'left':
|
||||
top = triggerRect.top + triggerRect.height / 2 - tooltipRect.height / 2;
|
||||
left = triggerRect.left - tooltipRect.width - offset;
|
||||
break;
|
||||
case 'top':
|
||||
top = triggerRect.top - tooltipRect.height - offset;
|
||||
left = triggerRect.left + triggerRect.width / 2 - tooltipRect.width / 2;
|
||||
break;
|
||||
case 'bottom':
|
||||
top = triggerRect.bottom + offset;
|
||||
left = triggerRect.left + triggerRect.width / 2 - tooltipRect.width / 2;
|
||||
break;
|
||||
}
|
||||
|
||||
return { top, left };
|
||||
}
|
||||
|
||||
export function useTooltipPosition({
|
||||
open,
|
||||
sidebarTooltip,
|
||||
position,
|
||||
gap,
|
||||
triggerRef,
|
||||
tooltipRef,
|
||||
sidebarRefs,
|
||||
sidebarState
|
||||
}: {
|
||||
open: boolean;
|
||||
sidebarTooltip: boolean;
|
||||
position: Position;
|
||||
gap: number;
|
||||
triggerRef: React.RefObject<HTMLElement | null>;
|
||||
tooltipRef: React.RefObject<HTMLDivElement | null>;
|
||||
sidebarRefs?: SidebarRefs;
|
||||
sidebarState?: SidebarState;
|
||||
}): PositionState {
|
||||
const [coords, setCoords] = useState<{ top: number; left: number; arrowOffset: number | null }>({
|
||||
top: 0,
|
||||
left: 0,
|
||||
arrowOffset: null
|
||||
});
|
||||
const [positionReady, setPositionReady] = useState(false);
|
||||
|
||||
// Fallback sidebar position (only used as last resort)
|
||||
const sidebarLeft = 240;
|
||||
|
||||
const updatePosition = () => {
|
||||
if (!triggerRef.current || !open) return;
|
||||
|
||||
const triggerRect = triggerRef.current.getBoundingClientRect();
|
||||
|
||||
let top: number;
|
||||
let left: number;
|
||||
let arrowOffset: number | null = null;
|
||||
|
||||
if (sidebarTooltip) {
|
||||
// Require sidebar refs and state for proper positioning
|
||||
if (!sidebarRefs || !sidebarState) {
|
||||
console.warn('⚠️ Sidebar tooltip requires sidebarRefs and sidebarState props');
|
||||
setPositionReady(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const sidebarInfo = getSidebarInfo(sidebarRefs, sidebarState);
|
||||
const currentSidebarRight = sidebarInfo.rect ? sidebarInfo.rect.right : sidebarLeft;
|
||||
|
||||
// Only show tooltip if we have the tool panel active
|
||||
if (!sidebarInfo.isToolPanelActive) {
|
||||
console.log('🚫 Not showing tooltip - tool panel not active');
|
||||
setPositionReady(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Position to the right of active sidebar with 20px gap
|
||||
left = currentSidebarRight + 20;
|
||||
top = triggerRect.top; // Align top of tooltip with trigger element
|
||||
|
||||
// Only clamp if we have tooltip dimensions
|
||||
if (tooltipRef.current) {
|
||||
const tooltipRect = tooltipRef.current.getBoundingClientRect();
|
||||
const maxTop = window.innerHeight - tooltipRect.height - 4;
|
||||
const originalTop = top;
|
||||
top = clamp(top, 4, maxTop);
|
||||
|
||||
// If tooltip was clamped, adjust arrow position to stay aligned with trigger
|
||||
if (originalTop !== top) {
|
||||
arrowOffset = triggerRect.top + triggerRect.height / 2 - top;
|
||||
}
|
||||
}
|
||||
|
||||
setCoords({ top, left, arrowOffset });
|
||||
setPositionReady(true);
|
||||
} else {
|
||||
// Regular tooltip logic
|
||||
if (!tooltipRef.current) return;
|
||||
|
||||
const tooltipRect = tooltipRef.current.getBoundingClientRect();
|
||||
const placement = place(triggerRect, tooltipRect, position, gap);
|
||||
top = placement.top;
|
||||
left = placement.left;
|
||||
|
||||
// Clamp to viewport
|
||||
top = clamp(top, 4, window.innerHeight - tooltipRect.height - 4);
|
||||
left = clamp(left, 4, window.innerWidth - tooltipRect.width - 4);
|
||||
|
||||
// Calculate arrow position to stay aligned with trigger
|
||||
if (position === 'top' || position === 'bottom') {
|
||||
// For top/bottom arrows, adjust horizontal position
|
||||
const triggerCenter = triggerRect.left + triggerRect.width / 2;
|
||||
const tooltipCenter = left + tooltipRect.width / 2;
|
||||
if (Math.abs(triggerCenter - tooltipCenter) > 4) {
|
||||
// Arrow needs adjustment
|
||||
arrowOffset = triggerCenter - left - 4; // 4px is half arrow width
|
||||
}
|
||||
} else {
|
||||
// For left/right arrows, adjust vertical position
|
||||
const triggerCenter = triggerRect.top + triggerRect.height / 2;
|
||||
const tooltipCenter = top + tooltipRect.height / 2;
|
||||
if (Math.abs(triggerCenter - tooltipCenter) > 4) {
|
||||
// Arrow needs adjustment
|
||||
arrowOffset = triggerCenter - top - 4; // 4px is half arrow height
|
||||
}
|
||||
}
|
||||
|
||||
setCoords({ top, left, arrowOffset });
|
||||
setPositionReady(true);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
|
||||
requestAnimationFrame(updatePosition);
|
||||
|
||||
const handleUpdate = () => requestAnimationFrame(updatePosition);
|
||||
window.addEventListener('scroll', handleUpdate, true);
|
||||
window.addEventListener('resize', handleUpdate);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', handleUpdate, true);
|
||||
window.removeEventListener('resize', handleUpdate);
|
||||
};
|
||||
}, [open, sidebarLeft, position, gap, sidebarTooltip]);
|
||||
|
||||
return { coords, positionReady };
|
||||
}
|
||||
Reference in New Issue
Block a user