From eee931e4a25cb0ec1f2dc79a76b11058c794899d Mon Sep 17 00:00:00 2001 From: EthanHealy01 Date: Thu, 7 Aug 2025 15:50:40 +0100 Subject: [PATCH] use refs rather than document query selection to find tooltip position (or just to find sidebar rect for furture features that require it) --- .../src/components/shared/QuickAccessBar.tsx | 35 ++-------- frontend/src/components/shared/Tooltip.tsx | 8 ++- .../shared/tooltip/Tooltip.README.md | 4 +- frontend/src/components/tips/CompressTips.ts | 2 +- frontend/src/components/tips/OCRTips.ts | 2 +- frontend/src/contexts/SidebarContext.tsx | 47 +++++++++++++ frontend/src/hooks/useTooltipPosition.ts | 39 ++++++----- frontend/src/pages/HomePage.tsx | 26 ++++--- frontend/src/types/sidebar.ts | 46 ++++++++++++ .../tips/types.ts => types/tips.ts} | 0 frontend/src/utils/sidebarUtils.ts | 70 +++++++------------ 11 files changed, 179 insertions(+), 100 deletions(-) create mode 100644 frontend/src/contexts/SidebarContext.tsx create mode 100644 frontend/src/types/sidebar.ts rename frontend/src/{components/tips/types.ts => types/tips.ts} (100%) diff --git a/frontend/src/components/shared/QuickAccessBar.tsx b/frontend/src/components/shared/QuickAccessBar.tsx index 28c0c7460..fb27b1c2c 100644 --- a/frontend/src/components/shared/QuickAccessBar.tsx +++ b/frontend/src/components/shared/QuickAccessBar.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef } from "react"; +import React, { useState, useRef, forwardRef } from "react"; import { ActionIcon, Stack, Tooltip, Divider } from "@mantine/core"; import MenuBookIcon from "@mui/icons-material/MenuBookRounded"; import AppsIcon from "@mui/icons-material/AppsRounded"; @@ -8,32 +8,12 @@ import FolderIcon from "@mui/icons-material/FolderRounded"; import PersonIcon from "@mui/icons-material/PersonRounded"; import NotificationsIcon from "@mui/icons-material/NotificationsRounded"; import { useRainbowThemeContext } from "./RainbowThemeProvider"; -import rainbowStyles from '../../styles/rainbow.module.css'; import AppConfigModal from './AppConfigModal'; import { useIsOverflowing } from '../../hooks/useIsOverflowing'; import { useFilesModalContext } from '../../contexts/FilesModalContext'; +import { QuickAccessBarProps, ButtonConfig } from '../../types/sidebar'; import './QuickAccessBar.css'; -interface QuickAccessBarProps { - onToolsClick: () => void; - onReaderToggle: () => void; - selectedToolKey?: string; - toolRegistry: any; - leftPanelView: 'toolPicker' | 'toolContent'; - readerMode: boolean; -} - -interface ButtonConfig { - id: string; - name: string; - icon: React.ReactNode; - tooltip: string; - isRound?: boolean; - size?: 'sm' | 'md' | 'lg' | 'xl'; - onClick: () => void; - type?: 'navigation' | 'modal' | 'action'; // navigation = main nav, modal = triggers modal, action = other actions -} - function NavHeader({ activeButton, setActiveButton, @@ -104,14 +84,10 @@ function NavHeader({ ); } -const QuickAccessBar = ({ +const QuickAccessBar = forwardRef(({ onToolsClick, onReaderToggle, - selectedToolKey, - toolRegistry, - leftPanelView, - readerMode, -}: QuickAccessBarProps) => { +}, ref) => { const { isRainbowMode } = useRainbowThemeContext(); const { openFilesModal, isFilesModalOpen } = useFilesModalContext(); const [configModalOpen, setConfigModalOpen] = useState(false); @@ -234,6 +210,7 @@ const QuickAccessBar = ({ return (
@@ -336,6 +313,6 @@ const QuickAccessBar = ({ />
); -}; +}); export default QuickAccessBar; \ No newline at end of file diff --git a/frontend/src/components/shared/Tooltip.tsx b/frontend/src/components/shared/Tooltip.tsx index 38060a300..6deda77c4 100644 --- a/frontend/src/components/shared/Tooltip.tsx +++ b/frontend/src/components/shared/Tooltip.tsx @@ -3,6 +3,7 @@ import { createPortal } from 'react-dom'; import { isClickOutside, addEventListenerWithCleanup } from '../../utils/genericUtils'; import { useTooltipPosition } from '../../hooks/useTooltipPosition'; import { TooltipContent, TooltipTip } from './tooltip/TooltipContent'; +import { useSidebarContext } from '../../contexts/SidebarContext'; import styles from './tooltip/Tooltip.module.css' export interface TooltipProps { @@ -42,6 +43,9 @@ export const Tooltip: React.FC = ({ const triggerRef = useRef(null); const tooltipRef = useRef(null); const hoverTimeoutRef = useRef | null>(null); + + // Get sidebar context for tooltip positioning + const sidebarContext = sidebarTooltip ? useSidebarContext() : null; // Always use controlled mode - if no controlled props provided, use internal state const isControlled = controlledOpen !== undefined; @@ -80,7 +84,9 @@ export const Tooltip: React.FC = ({ position, gap, triggerRef, - tooltipRef + tooltipRef, + sidebarRefs: sidebarContext?.sidebarRefs, + sidebarState: sidebarContext?.sidebarState }); // Add document click listener for unpinning diff --git a/frontend/src/components/shared/tooltip/Tooltip.README.md b/frontend/src/components/shared/tooltip/Tooltip.README.md index 50a4037c0..df1a977b0 100644 --- a/frontend/src/components/shared/tooltip/Tooltip.README.md +++ b/frontend/src/components/shared/tooltip/Tooltip.README.md @@ -217,5 +217,7 @@ Links automatically get proper styling with hover states and open in new tabs wh ### Sidebar Tooltips - When `sidebarTooltip={true}`, horizontal positioning is locked to the right of the sidebar - Vertical positioning follows the trigger but clamps to viewport -- Automatically detects sidebar width or falls back to 240px +- **Smart sidebar detection**: Uses `getSidebarInfo()` to determine which sidebar is active (tool panel vs quick access bar) and gets its exact position +- **Dynamic positioning**: Adapts to whether the tool panel is expanded or collapsed +- **Conditional display**: Only shows tooltips when the tool panel is active (`sidebarInfo.isToolPanelActive`) - **No arrows** - sidebar tooltips don't show arrows diff --git a/frontend/src/components/tips/CompressTips.ts b/frontend/src/components/tips/CompressTips.ts index 62d8a0972..2fb2a0777 100644 --- a/frontend/src/components/tips/CompressTips.ts +++ b/frontend/src/components/tips/CompressTips.ts @@ -1,5 +1,5 @@ import { useTranslation } from 'react-i18next'; -import { TooltipContent } from './types'; +import { TooltipContent } from '../../types/tips'; export const CompressTips = (): TooltipContent => { const { t } = useTranslation(); diff --git a/frontend/src/components/tips/OCRTips.ts b/frontend/src/components/tips/OCRTips.ts index 1d5c7015a..1002182f2 100644 --- a/frontend/src/components/tips/OCRTips.ts +++ b/frontend/src/components/tips/OCRTips.ts @@ -1,5 +1,5 @@ import { useTranslation } from 'react-i18next'; -import { TooltipContent } from './types'; +import { TooltipContent } from '../../types/tips'; export const OcrTips = (): TooltipContent => { const { t } = useTranslation(); diff --git a/frontend/src/contexts/SidebarContext.tsx b/frontend/src/contexts/SidebarContext.tsx new file mode 100644 index 000000000..f09815c5c --- /dev/null +++ b/frontend/src/contexts/SidebarContext.tsx @@ -0,0 +1,47 @@ +import React, { createContext, useContext, useState, useRef } from 'react'; +import { SidebarState, SidebarRefs, SidebarContextValue, SidebarProviderProps } from '../types/sidebar'; + +const SidebarContext = createContext(undefined); + +export function SidebarProvider({ children }: SidebarProviderProps) { + // All sidebar state management + const quickAccessRef = useRef(null); + const toolPanelRef = useRef(null); + + const [sidebarsVisible, setSidebarsVisible] = useState(true); + const [leftPanelView, setLeftPanelView] = useState<'toolPicker' | 'toolContent'>('toolPicker'); + const [readerMode, setReaderMode] = useState(false); + + const sidebarState: SidebarState = { + sidebarsVisible, + leftPanelView, + readerMode, + }; + + const sidebarRefs: SidebarRefs = { + quickAccessRef, + toolPanelRef, + }; + + const contextValue: SidebarContextValue = { + sidebarState, + sidebarRefs, + setSidebarsVisible, + setLeftPanelView, + setReaderMode, + }; + + return ( + + {children} + + ); +} + +export function useSidebarContext(): SidebarContextValue { + const context = useContext(SidebarContext); + if (context === undefined) { + throw new Error('useSidebarContext must be used within a SidebarProvider'); + } + return context; +} \ No newline at end of file diff --git a/frontend/src/hooks/useTooltipPosition.ts b/frontend/src/hooks/useTooltipPosition.ts index 680db614c..3651c1d47 100644 --- a/frontend/src/hooks/useTooltipPosition.ts +++ b/frontend/src/hooks/useTooltipPosition.ts @@ -1,6 +1,7 @@ import { useState, useEffect, useMemo } from 'react'; import { clamp } from '../utils/genericUtils'; -import { getSidebarRect } from '../utils/sidebarUtils'; +import { getSidebarInfo } from '../utils/sidebarUtils'; +import { SidebarRefs, SidebarState } from '../types/sidebar'; type Position = 'right' | 'left' | 'top' | 'bottom'; @@ -51,7 +52,9 @@ export function useTooltipPosition({ position, gap, triggerRef, - tooltipRef + tooltipRef, + sidebarRefs, + sidebarState }: { open: boolean; sidebarTooltip: boolean; @@ -59,6 +62,8 @@ export function useTooltipPosition({ gap: number; triggerRef: React.RefObject; tooltipRef: React.RefObject; + sidebarRefs?: SidebarRefs; + sidebarState?: SidebarState; }): PositionState { const [coords, setCoords] = useState<{ top: number; left: number; arrowOffset: number | null }>({ top: 0, @@ -67,12 +72,8 @@ export function useTooltipPosition({ }); const [positionReady, setPositionReady] = useState(false); - // Memoize sidebar position for performance - const sidebarLeft = useMemo(() => { - if (!sidebarTooltip) return 0; - const sidebarInfo = getSidebarRect(); - return sidebarInfo.rect ? sidebarInfo.rect.right : 240; - }, [sidebarTooltip]); + // Fallback sidebar position (only used as last resort) + const sidebarLeft = 240; const updatePosition = () => { if (!triggerRef.current || !open) return; @@ -84,18 +85,24 @@ export function useTooltipPosition({ let arrowOffset: number | null = null; if (sidebarTooltip) { - // Get fresh sidebar position each time - const sidebarInfo = getSidebarRect(); - const currentSidebarRight = sidebarInfo.rect ? sidebarInfo.rect.right : sidebarLeft; - - // Only show tooltip if we have the correct sidebar (ToolPanel) - if (!sidebarInfo.isCorrectSidebar) { - console.log('🚫 Not showing tooltip - wrong sidebar detected'); + // Require sidebar refs and state for proper positioning + if (!sidebarRefs || !sidebarState) { + console.warn('⚠️ Sidebar tooltip requires sidebarRefs and sidebarState props'); setPositionReady(false); return; } - // Position to the right of correct sidebar with 20px gap + 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 diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index b0d040731..94a81ee6d 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -2,11 +2,13 @@ import React, { useState, useCallback, useEffect, useRef } from "react"; import { useTranslation } from 'react-i18next'; import { useFileContext } from "../contexts/FileContext"; import { FileSelectionProvider, useFileSelection } from "../contexts/FileSelectionContext"; +import { SidebarProvider, useSidebarContext } from "../contexts/SidebarContext"; import { useToolManagement } from "../hooks/useToolManagement"; import { useFileHandler } from "../hooks/useFileHandler"; import { Group, Box, Button } from "@mantine/core"; import { useRainbowThemeContext } from "../components/shared/RainbowThemeProvider"; import { PageEditorFunctions } from "../types/pageEditor"; +import { SidebarRefs, SidebarState } from "../types/sidebar"; import rainbowStyles from '../styles/rainbow.module.css'; import ToolPicker from "../components/tools/ToolPicker"; @@ -20,9 +22,20 @@ import QuickAccessBar from "../components/shared/QuickAccessBar"; import LandingPage from "../components/shared/LandingPage"; import FileUploadModal from "../components/shared/FileUploadModal"; + function HomePageContent() { const { t } = useTranslation(); const { isRainbowMode } = useRainbowThemeContext(); + const { + sidebarState, + sidebarRefs, + setSidebarsVisible, + setLeftPanelView, + setReaderMode + } = useSidebarContext(); + + const { sidebarsVisible, leftPanelView, readerMode } = sidebarState; + const { quickAccessRef, toolPanelRef } = sidebarRefs; const fileContext = useFileContext(); const { activeFiles, currentView, setCurrentView } = fileContext; @@ -37,9 +50,6 @@ function HomePageContent() { clearToolSelection, } = useToolManagement(); - const [sidebarsVisible, setSidebarsVisible] = useState(true); - const [leftPanelView, setLeftPanelView] = useState<'toolPicker' | 'toolContent'>('toolPicker'); - const [readerMode, setReaderMode] = useState(false); const [pageEditorFunctions, setPageEditorFunctions] = useState(null); const [previewFile, setPreviewFile] = useState(null); @@ -92,16 +102,14 @@ function HomePageContent() { > {/* Quick Access Bar */} {/* Left: Tool Picker or Selected Tool Panel */}
- + + + ); } diff --git a/frontend/src/types/sidebar.ts b/frontend/src/types/sidebar.ts new file mode 100644 index 000000000..60dcd029d --- /dev/null +++ b/frontend/src/types/sidebar.ts @@ -0,0 +1,46 @@ +export interface SidebarState { + sidebarsVisible: boolean; + leftPanelView: 'toolPicker' | 'toolContent'; + readerMode: boolean; +} + +export interface SidebarRefs { + quickAccessRef: React.RefObject; + toolPanelRef: React.RefObject; +} + +export interface SidebarInfo { + rect: DOMRect | null; + isToolPanelActive: boolean; + sidebarState: SidebarState; +} + +// Context-related interfaces +export interface SidebarContextValue { + sidebarState: SidebarState; + sidebarRefs: SidebarRefs; + setSidebarsVisible: React.Dispatch>; + setLeftPanelView: React.Dispatch>; + setReaderMode: React.Dispatch>; +} + +export interface SidebarProviderProps { + children: React.ReactNode; +} + +// QuickAccessBar related interfaces +export interface QuickAccessBarProps { + onToolsClick: () => void; + onReaderToggle: () => void; +} + +export interface ButtonConfig { + id: string; + name: string; + icon: React.ReactNode; + tooltip: string; + isRound?: boolean; + size?: 'sm' | 'md' | 'lg' | 'xl'; + onClick: () => void; + type?: 'navigation' | 'modal' | 'action'; +} diff --git a/frontend/src/components/tips/types.ts b/frontend/src/types/tips.ts similarity index 100% rename from frontend/src/components/tips/types.ts rename to frontend/src/types/tips.ts diff --git a/frontend/src/utils/sidebarUtils.ts b/frontend/src/utils/sidebarUtils.ts index 0c17b0876..cef144971 100644 --- a/frontend/src/utils/sidebarUtils.ts +++ b/frontend/src/utils/sidebarUtils.ts @@ -1,50 +1,34 @@ +import { SidebarRefs, SidebarState, SidebarInfo } from '../types/sidebar'; /** - * Gets the All tools sidebar - * @returns Object containing the sidebar rect and whether it's the correct sidebar + * Gets the All tools sidebar information using React refs and state + * @param refs - Object containing refs to sidebar elements + * @param state - Current sidebar state + * @returns Object containing the sidebar rect and whether the tool panel is active */ -export function getSidebarRect(): { rect: DOMRect | null, isCorrectSidebar: boolean } { - // Find the rightmost sidebar - this will be the "All Tools" expanded panel - const allSidebars = []; +export function getSidebarInfo(refs: SidebarRefs, state: SidebarState): SidebarInfo { + const { quickAccessRef, toolPanelRef } = refs; + const { sidebarsVisible, readerMode } = state; - // Find the QuickAccessBar (narrow left bar) - const quickAccessBar = document.querySelector('[data-sidebar="quick-access"]'); - if (quickAccessBar) { - const rect = quickAccessBar.getBoundingClientRect(); - if (rect.width > 0) { - allSidebars.push({ - element: 'QuickAccessBar', - selector: '[data-sidebar="quick-access"]', - rect - }); - } - } + // Determine if tool panel should be active based on state + const isToolPanelActive = sidebarsVisible && !readerMode; - // Find the tool panel (the expanded "All Tools" panel) - const toolPanel = document.querySelector('[data-sidebar="tool-panel"]'); - if (toolPanel) { - const rect = toolPanel.getBoundingClientRect(); - if (rect.width > 0) { - allSidebars.push({ - element: 'ToolPanel', - selector: '[data-sidebar="tool-panel"]', - rect - }); - } - } + let rect: DOMRect | null = null; - // Use the rightmost sidebar (which should be the tool panel when expanded) - if (allSidebars.length > 0) { - const rightmostSidebar = allSidebars.reduce((rightmost, current) => { - return current.rect.right > rightmost.rect.right ? current : rightmost; - }); + if (isToolPanelActive && toolPanelRef.current) { + // Tool panel is expanded: use its rect + rect = toolPanelRef.current.getBoundingClientRect(); + } else if (quickAccessRef.current) { + // Fall back to quick access bar + // This probably isn't needed but if we ever have tooltips or modals that need to be positioned relative to the quick access bar, we can use this + rect = quickAccessRef.current.getBoundingClientRect(); + } - // Only consider it correct if we're using the ToolPanel (expanded All Tools sidebar) - const isCorrectSidebar = rightmostSidebar.element === 'ToolPanel'; - return { rect: rightmostSidebar.rect, isCorrectSidebar }; - } - - console.warn('⚠️ No sidebars found, using fallback positioning'); - // Final fallback - return { rect: new DOMRect(0, 0, 280, window.innerHeight), isCorrectSidebar: false }; - } \ No newline at end of file + return { + rect, + isToolPanelActive, + sidebarState: state + }; +} + + \ No newline at end of file