mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-02-17 13:52:14 +01:00
change bulk selection panel to allow more versatile input (#4394)
# Description of Changes - Add features to BulkSelectionPanel to allow more versatility when selecting pages - Make changes to Tooltip to: Remove non-existent props delayAppearance, fixed defaults no hardcoded maxWidth, and documented new props (closeOnOutside, containerStyle, minWidth). Clarify pinned vs. unpinned outside-click logic, hover/focus interactions, and event/ref preservation. - Made top controls show full text always rather than dynamically display the text only for the selected items --- ## 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:
@@ -11,6 +11,7 @@ import LanguageSelector from '../shared/LanguageSelector';
|
||||
import { useRainbowThemeContext } from '../shared/RainbowThemeProvider';
|
||||
import { Tooltip } from '../shared/Tooltip';
|
||||
import BulkSelectionPanel from '../pageEditor/BulkSelectionPanel';
|
||||
import { parseSelection } from '../../utils/bulkselection/parseSelection';
|
||||
|
||||
export default function RightRail() {
|
||||
const { t } = useTranslation();
|
||||
@@ -111,50 +112,13 @@ export default function RightRail() {
|
||||
setSelectedFiles([]);
|
||||
}, [currentView, selectedFileIds, removeFiles, setSelectedFiles]);
|
||||
|
||||
// CSV parsing functions for page selection
|
||||
const parseCSVInput = useCallback((csv: string) => {
|
||||
const pageNumbers: number[] = [];
|
||||
const ranges = csv.split(',').map(s => s.trim()).filter(Boolean);
|
||||
|
||||
ranges.forEach(range => {
|
||||
if (range.includes('-')) {
|
||||
const [start, end] = range.split('-').map(n => parseInt(n.trim()));
|
||||
for (let i = start; i <= end; i++) {
|
||||
if (i > 0) {
|
||||
pageNumbers.push(i);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const pageNum = parseInt(range);
|
||||
if (pageNum > 0) {
|
||||
pageNumbers.push(pageNum);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return pageNumbers;
|
||||
}, []);
|
||||
|
||||
const updatePagesFromCSV = useCallback(() => {
|
||||
const rawPages = parseCSVInput(csvInput);
|
||||
// Use PageEditor's total pages for validation
|
||||
const updatePagesFromCSV = useCallback((override?: string) => {
|
||||
const maxPages = pageEditorFunctions?.totalPages || 0;
|
||||
const normalized = Array.from(new Set(rawPages.filter(n => Number.isFinite(n) && n > 0 && n <= maxPages))).sort((a,b)=>a-b);
|
||||
// Use PageEditor's function to set selected pages
|
||||
const normalized = parseSelection(override ?? csvInput, maxPages);
|
||||
pageEditorFunctions?.handleSetSelectedPages?.(normalized);
|
||||
}, [csvInput, parseCSVInput, pageEditorFunctions]);
|
||||
}, [csvInput, pageEditorFunctions]);
|
||||
|
||||
// Sync csvInput with PageEditor's selected pages
|
||||
useEffect(() => {
|
||||
const sortedPageNumbers = Array.isArray(pageEditorFunctions?.selectedPageIds) && pageEditorFunctions.displayDocument
|
||||
? pageEditorFunctions.selectedPageIds.map(id => {
|
||||
const page = pageEditorFunctions.displayDocument!.pages.find(p => p.id === id);
|
||||
return page?.pageNumber || 0;
|
||||
}).filter(num => num > 0).sort((a, b) => a - b)
|
||||
: [];
|
||||
const newCsvInput = sortedPageNumbers.join(', ');
|
||||
setCsvInput(newCsvInput);
|
||||
}, [pageEditorFunctions?.selectedPageIds]);
|
||||
// Do not overwrite user's expression input when selection changes.
|
||||
|
||||
// Clear CSV input when files change (use stable signature to avoid ref churn)
|
||||
useEffect(() => {
|
||||
@@ -260,7 +224,7 @@ export default function RightRail() {
|
||||
</div>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<div style={{ minWidth: 280 }}>
|
||||
<div style={{ minWidth: '24rem', maxWidth: '32rem' }}>
|
||||
<BulkSelectionPanel
|
||||
csvInput={csvInput}
|
||||
setCsvInput={setCsvInput}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import LocalIcon from './LocalIcon';
|
||||
import { isClickOutside, addEventListenerWithCleanup } from '../../utils/genericUtils';
|
||||
import { addEventListenerWithCleanup } from '../../utils/genericUtils';
|
||||
import { useTooltipPosition } from '../../hooks/useTooltipPosition';
|
||||
import { TooltipTip } from '../../types/tips';
|
||||
import { TooltipContent } from './tooltip/TooltipContent';
|
||||
import { useSidebarContext } from '../../contexts/SidebarContext';
|
||||
import styles from './tooltip/Tooltip.module.css'
|
||||
import styles from './tooltip/Tooltip.module.css';
|
||||
|
||||
export interface TooltipProps {
|
||||
sidebarTooltip?: boolean;
|
||||
@@ -21,12 +21,12 @@ export interface TooltipProps {
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
arrow?: boolean;
|
||||
portalTarget?: HTMLElement;
|
||||
header?: {
|
||||
title: string;
|
||||
logo?: React.ReactNode;
|
||||
};
|
||||
header?: { title: string; logo?: React.ReactNode };
|
||||
delay?: number;
|
||||
containerStyle?: React.CSSProperties;
|
||||
pinOnClick?: boolean;
|
||||
/** If true, clicking outside also closes when not pinned (default true) */
|
||||
closeOnOutside?: boolean;
|
||||
}
|
||||
|
||||
export const Tooltip: React.FC<TooltipProps> = ({
|
||||
@@ -44,57 +44,41 @@ export const Tooltip: React.FC<TooltipProps> = ({
|
||||
portalTarget,
|
||||
header,
|
||||
delay = 0,
|
||||
containerStyle={},
|
||||
containerStyle = {},
|
||||
pinOnClick = false,
|
||||
closeOnOutside = true,
|
||||
}) => {
|
||||
const [internalOpen, setInternalOpen] = useState(false);
|
||||
const [isPinned, setIsPinned] = useState(false);
|
||||
const triggerRef = useRef<HTMLElement>(null);
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const triggerRef = useRef<HTMLElement | null>(null);
|
||||
const tooltipRef = useRef<HTMLDivElement | null>(null);
|
||||
const openTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const clearTimers = () => {
|
||||
const clickPendingRef = useRef(false);
|
||||
const tooltipIdRef = useRef(`tooltip-${Math.random().toString(36).slice(2)}`);
|
||||
|
||||
const clearTimers = useCallback(() => {
|
||||
if (openTimeoutRef.current) {
|
||||
clearTimeout(openTimeoutRef.current);
|
||||
openTimeoutRef.current = 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;
|
||||
const open = isControlled ? controlledOpen : internalOpen;
|
||||
const open = isControlled ? !!controlledOpen : internalOpen;
|
||||
|
||||
const handleOpenChange = (newOpen: boolean) => {
|
||||
clearTimers();
|
||||
if (isControlled) {
|
||||
onOpenChange?.(newOpen);
|
||||
} else {
|
||||
setInternalOpen(newOpen);
|
||||
}
|
||||
const setOpen = useCallback(
|
||||
(newOpen: boolean) => {
|
||||
if (newOpen === open) return; // avoid churn
|
||||
if (isControlled) onOpenChange?.(newOpen);
|
||||
else setInternalOpen(newOpen);
|
||||
if (!newOpen) setIsPinned(false);
|
||||
},
|
||||
[isControlled, onOpenChange, open]
|
||||
);
|
||||
|
||||
// Reset pin state when closing
|
||||
if (!newOpen) {
|
||||
setIsPinned(false);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const handleTooltipClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setIsPinned(true);
|
||||
};
|
||||
|
||||
const handleDocumentClick = (e: MouseEvent) => {
|
||||
// If tooltip is pinned and we click outside of it, unpin it
|
||||
if (isPinned && isClickOutside(e, tooltipRef.current)) {
|
||||
setIsPinned(false);
|
||||
handleOpenChange(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Use the positioning hook
|
||||
const { coords, positionReady } = useTooltipPosition({
|
||||
open,
|
||||
sidebarTooltip,
|
||||
@@ -103,56 +87,209 @@ export const Tooltip: React.FC<TooltipProps> = ({
|
||||
triggerRef,
|
||||
tooltipRef,
|
||||
sidebarRefs: sidebarContext?.sidebarRefs,
|
||||
sidebarState: sidebarContext?.sidebarState
|
||||
sidebarState: sidebarContext?.sidebarState,
|
||||
});
|
||||
|
||||
// Add document click listener for unpinning
|
||||
// Close on outside click: pinned → close; not pinned → optionally close
|
||||
const handleDocumentClick = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
const tEl = tooltipRef.current;
|
||||
const trg = triggerRef.current;
|
||||
const target = e.target as Node | null;
|
||||
const insideTooltip = tEl && target && tEl.contains(target);
|
||||
const insideTrigger = trg && target && trg.contains(target);
|
||||
|
||||
// If pinned: only close when clicking outside BOTH tooltip & trigger
|
||||
if (isPinned) {
|
||||
if (!insideTooltip && !insideTrigger) {
|
||||
setIsPinned(false);
|
||||
setOpen(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Not pinned and configured to close on outside
|
||||
if (closeOnOutside && !insideTooltip && !insideTrigger) {
|
||||
setOpen(false);
|
||||
}
|
||||
},
|
||||
[isPinned, closeOnOutside, setOpen]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isPinned) {
|
||||
// Attach global click when open (so hover tooltips can also close on outside if desired)
|
||||
if (open || isPinned) {
|
||||
return addEventListenerWithCleanup(document, 'click', handleDocumentClick as EventListener);
|
||||
}
|
||||
}, [isPinned]);
|
||||
}, [open, isPinned, handleDocumentClick]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearTimers();
|
||||
};
|
||||
}, []);
|
||||
useEffect(() => () => clearTimers(), [clearTimers]);
|
||||
|
||||
|
||||
const getArrowClass = () => {
|
||||
// No arrow for sidebar tooltips
|
||||
const arrowClass = useMemo(() => {
|
||||
if (sidebarTooltip) return null;
|
||||
const map: Record<NonNullable<TooltipProps['position']>, string> = {
|
||||
top: 'tooltip-arrow-bottom',
|
||||
bottom: 'tooltip-arrow-top',
|
||||
left: 'tooltip-arrow-left',
|
||||
right: 'tooltip-arrow-right',
|
||||
};
|
||||
return map[position] || map.right;
|
||||
}, [position, sidebarTooltip]);
|
||||
|
||||
switch (position) {
|
||||
case 'top': return "tooltip-arrow tooltip-arrow-bottom";
|
||||
case 'bottom': return "tooltip-arrow tooltip-arrow-top";
|
||||
case 'left': return "tooltip-arrow tooltip-arrow-left";
|
||||
case 'right': return "tooltip-arrow tooltip-arrow-right";
|
||||
default: return "tooltip-arrow tooltip-arrow-right";
|
||||
}
|
||||
};
|
||||
const getArrowStyleClass = useCallback(
|
||||
(key: string) =>
|
||||
styles[key as keyof typeof styles] ||
|
||||
styles[key.replace(/-([a-z])/g, (_, l) => l.toUpperCase()) as keyof typeof styles] ||
|
||||
'',
|
||||
[]
|
||||
);
|
||||
|
||||
const getArrowStyleClass = (arrowClass: string) => {
|
||||
const styleKey = arrowClass.split(' ')[1];
|
||||
// Handle both kebab-case and camelCase CSS module exports
|
||||
return styles[styleKey as keyof typeof styles] ||
|
||||
styles[styleKey.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()) as keyof typeof styles] ||
|
||||
'';
|
||||
};
|
||||
// === Trigger handlers ===
|
||||
const openWithDelay = useCallback(() => {
|
||||
clearTimers();
|
||||
openTimeoutRef.current = setTimeout(() => setOpen(true), Math.max(0, delay || 0));
|
||||
}, [clearTimers, setOpen, delay]);
|
||||
|
||||
const handlePointerEnter = useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
if (!isPinned) openWithDelay();
|
||||
(children.props as any)?.onPointerEnter?.(e);
|
||||
},
|
||||
[isPinned, openWithDelay, children.props]
|
||||
);
|
||||
|
||||
const handlePointerLeave = useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
const related = e.relatedTarget as Node | null;
|
||||
|
||||
// Moving into the tooltip → keep open
|
||||
if (related && tooltipRef.current && tooltipRef.current.contains(related)) {
|
||||
(children.props as any)?.onPointerLeave?.(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore transient leave between mousedown and click
|
||||
if (clickPendingRef.current) {
|
||||
(children.props as any)?.onPointerLeave?.(e);
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimers();
|
||||
if (!isPinned) setOpen(false);
|
||||
(children.props as any)?.onPointerLeave?.(e);
|
||||
},
|
||||
[clearTimers, isPinned, setOpen, children.props]
|
||||
);
|
||||
|
||||
const handleMouseDown = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
clickPendingRef.current = true;
|
||||
(children.props as any)?.onMouseDown?.(e);
|
||||
},
|
||||
[children.props]
|
||||
);
|
||||
|
||||
const handleMouseUp = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
// allow microtask turn so click can see this false
|
||||
queueMicrotask(() => (clickPendingRef.current = false));
|
||||
(children.props as any)?.onMouseUp?.(e);
|
||||
},
|
||||
[children.props]
|
||||
);
|
||||
|
||||
const handleClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
clearTimers();
|
||||
if (pinOnClick) {
|
||||
e.preventDefault?.();
|
||||
e.stopPropagation?.();
|
||||
if (!open) setOpen(true);
|
||||
setIsPinned(true);
|
||||
clickPendingRef.current = false;
|
||||
return;
|
||||
}
|
||||
clickPendingRef.current = false;
|
||||
(children.props as any)?.onClick?.(e);
|
||||
},
|
||||
[clearTimers, pinOnClick, open, setOpen, children.props]
|
||||
);
|
||||
|
||||
// Keyboard / focus accessibility
|
||||
const handleFocus = useCallback(
|
||||
(e: React.FocusEvent) => {
|
||||
if (!isPinned) openWithDelay();
|
||||
(children.props as any)?.onFocus?.(e);
|
||||
},
|
||||
[isPinned, openWithDelay, children.props]
|
||||
);
|
||||
|
||||
const handleBlur = useCallback(
|
||||
(e: React.FocusEvent) => {
|
||||
const related = e.relatedTarget as Node | null;
|
||||
if (related && tooltipRef.current && tooltipRef.current.contains(related)) {
|
||||
(children.props as any)?.onBlur?.(e);
|
||||
return;
|
||||
}
|
||||
if (!isPinned) setOpen(false);
|
||||
(children.props as any)?.onBlur?.(e);
|
||||
},
|
||||
[isPinned, setOpen, children.props]
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Escape') setOpen(false);
|
||||
}, [setOpen]);
|
||||
|
||||
// Keep open while pointer is over the tooltip; close when leaving it (if not pinned)
|
||||
const handleTooltipPointerEnter = useCallback(() => {
|
||||
clearTimers();
|
||||
}, [clearTimers]);
|
||||
|
||||
const handleTooltipPointerLeave = useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
const related = e.relatedTarget as Node | null;
|
||||
if (related && triggerRef.current && triggerRef.current.contains(related)) return;
|
||||
if (!isPinned) setOpen(false);
|
||||
},
|
||||
[isPinned, setOpen]
|
||||
);
|
||||
|
||||
// Enhance child with handlers and ref
|
||||
const childWithHandlers = React.cloneElement(children as any, {
|
||||
ref: (node: HTMLElement | null) => {
|
||||
triggerRef.current = node || null;
|
||||
const originalRef = (children as any).ref;
|
||||
if (typeof originalRef === 'function') originalRef(node);
|
||||
else if (originalRef && typeof originalRef === 'object') (originalRef as any).current = node;
|
||||
},
|
||||
'aria-describedby': open ? tooltipIdRef.current : undefined,
|
||||
onPointerEnter: handlePointerEnter,
|
||||
onPointerLeave: handlePointerLeave,
|
||||
onMouseDown: handleMouseDown,
|
||||
onMouseUp: handleMouseUp,
|
||||
onClick: handleClick,
|
||||
onFocus: handleFocus,
|
||||
onBlur: handleBlur,
|
||||
onKeyDown: handleKeyDown,
|
||||
});
|
||||
|
||||
// Always mount when open so we can measure; hide until positioned to avoid flash
|
||||
const shouldShowTooltip = open;
|
||||
|
||||
const tooltipElement = shouldShowTooltip ? (
|
||||
<div
|
||||
id={tooltipIdRef.current}
|
||||
ref={tooltipRef}
|
||||
role="tooltip"
|
||||
tabIndex={-1}
|
||||
onPointerEnter={handleTooltipPointerEnter}
|
||||
onPointerLeave={handleTooltipPointerLeave}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: coords.top,
|
||||
left: coords.left,
|
||||
width: (maxWidth !== undefined ? maxWidth : (sidebarTooltip ? '25rem' : undefined)),
|
||||
minWidth: minWidth,
|
||||
width: maxWidth !== undefined ? maxWidth : (sidebarTooltip ? '25rem' as const : undefined),
|
||||
minWidth,
|
||||
zIndex: 9999,
|
||||
visibility: positionReady ? 'visible' : 'hidden',
|
||||
opacity: positionReady ? 1 : 0,
|
||||
@@ -160,7 +297,7 @@ export const Tooltip: React.FC<TooltipProps> = ({
|
||||
...containerStyle,
|
||||
}}
|
||||
className={`${styles['tooltip-container']} ${isPinned ? styles.pinned : ''}`}
|
||||
onClick={handleTooltipClick}
|
||||
onClick={pinOnClick ? (e) => { e.stopPropagation(); setIsPinned(true); } : undefined}
|
||||
>
|
||||
{isPinned && (
|
||||
<button
|
||||
@@ -168,97 +305,48 @@ export const Tooltip: React.FC<TooltipProps> = ({
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsPinned(false);
|
||||
handleOpenChange(false);
|
||||
setOpen(false);
|
||||
}}
|
||||
title="Close tooltip"
|
||||
aria-label="Close tooltip"
|
||||
>
|
||||
<LocalIcon icon="close-rounded" width="1.25rem" height="1.25rem" />
|
||||
</button>
|
||||
)}
|
||||
{arrow && getArrowClass() && (
|
||||
{arrow && !sidebarTooltip && (
|
||||
<div
|
||||
className={`${styles['tooltip-arrow']} ${getArrowStyleClass(getArrowClass()!)}`}
|
||||
style={coords.arrowOffset !== null ? {
|
||||
[position === 'top' || position === 'bottom' ? 'left' : 'top']: coords.arrowOffset
|
||||
} : undefined}
|
||||
className={`${styles['tooltip-arrow']} ${getArrowStyleClass(arrowClass!)}`}
|
||||
style={
|
||||
coords.arrowOffset !== null
|
||||
? { [position === 'top' || position === 'bottom' ? 'left' : 'top']: coords.arrowOffset }
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{header && (
|
||||
<div className={styles['tooltip-header']}>
|
||||
<div className={styles['tooltip-logo']}>
|
||||
{header.logo || <img src="/logo-tooltip.svg" alt="Stirling PDF" style={{ width: '1.4rem', height: '1.4rem', display: 'block' }} />}
|
||||
{header.logo || (
|
||||
<img
|
||||
src="/logo-tooltip.svg"
|
||||
alt="Stirling PDF"
|
||||
style={{ width: '1.4rem', height: '1.4rem', display: 'block' }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<span className={styles['tooltip-title']}>{header.title}</span>
|
||||
</div>
|
||||
)}
|
||||
<TooltipContent
|
||||
content={content}
|
||||
tips={tips}
|
||||
/>
|
||||
<TooltipContent content={content} tips={tips} />
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const handleMouseEnter = (e: React.MouseEvent) => {
|
||||
clearTimers();
|
||||
if (!isPinned) {
|
||||
const effectiveDelay = Math.max(0, delay || 0);
|
||||
openTimeoutRef.current = setTimeout(() => {
|
||||
handleOpenChange(true);
|
||||
}, effectiveDelay);
|
||||
}
|
||||
|
||||
(children.props as any)?.onMouseEnter?.(e);
|
||||
};
|
||||
|
||||
const handleMouseLeave = (e: React.MouseEvent) => {
|
||||
clearTimers();
|
||||
openTimeoutRef.current = null;
|
||||
|
||||
if (!isPinned) {
|
||||
handleOpenChange(false);
|
||||
}
|
||||
|
||||
(children.props as any)?.onMouseLeave?.(e);
|
||||
};
|
||||
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
// Toggle pin state on click
|
||||
if (open) {
|
||||
setIsPinned(!isPinned);
|
||||
} else {
|
||||
clearTimers();
|
||||
handleOpenChange(true);
|
||||
setIsPinned(true);
|
||||
}
|
||||
|
||||
(children.props as any)?.onClick?.(e);
|
||||
};
|
||||
|
||||
// Take the child element and add tooltip behavior to it
|
||||
const childWithTooltipHandlers = React.cloneElement(children as any, {
|
||||
// Keep track of the element for positioning
|
||||
ref: (node: HTMLElement) => {
|
||||
triggerRef.current = node;
|
||||
// Don't break if the child already has a ref
|
||||
const originalRef = (children as any).ref;
|
||||
if (typeof originalRef === 'function') {
|
||||
originalRef(node);
|
||||
} else if (originalRef && typeof originalRef === 'object') {
|
||||
originalRef.current = node;
|
||||
}
|
||||
},
|
||||
// Add mouse events to show/hide tooltip
|
||||
onMouseEnter: handleMouseEnter,
|
||||
onMouseLeave: handleMouseLeave,
|
||||
onClick: handleClick,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{childWithTooltipHandlers}
|
||||
{childWithHandlers}
|
||||
{portalTarget && document.body.contains(portalTarget)
|
||||
? tooltipElement && createPortal(tooltipElement, portalTarget)
|
||||
: tooltipElement}
|
||||
</>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import VisibilityIcon from "@mui/icons-material/Visibility";
|
||||
import EditNoteIcon from "@mui/icons-material/EditNote";
|
||||
import FolderIcon from "@mui/icons-material/Folder";
|
||||
import { WorkbenchType, isValidWorkbench } from '../../types/workbench';
|
||||
import { Tooltip } from "./Tooltip";
|
||||
|
||||
|
||||
const viewOptionStyle = {
|
||||
display: 'inline-flex',
|
||||
@@ -18,7 +18,7 @@ const viewOptionStyle = {
|
||||
}
|
||||
|
||||
|
||||
// Build view options showing text only for current view; others icon-only with tooltip
|
||||
// Build view options showing text always
|
||||
const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchType | null) => [
|
||||
{
|
||||
label: (
|
||||
@@ -35,35 +35,37 @@ const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchTyp
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<Tooltip content="Page Editor" position="bottom" arrow={true}>
|
||||
<div style={viewOptionStyle as React.CSSProperties}>
|
||||
{currentView === "pageEditor" ? (
|
||||
<>
|
||||
{switchingTo === "pageEditor" ? <Loader size="xs" /> : <EditNoteIcon fontSize="small" />}
|
||||
<span>Page Editor</span>
|
||||
</>
|
||||
) : (
|
||||
switchingTo === "pageEditor" ? <Loader size="xs" /> : <EditNoteIcon fontSize="small" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div style={viewOptionStyle as React.CSSProperties}>
|
||||
{currentView === "pageEditor" ? (
|
||||
<>
|
||||
{switchingTo === "pageEditor" ? <Loader size="xs" /> : <EditNoteIcon fontSize="small" />}
|
||||
<span>Page Editor</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{switchingTo === "pageEditor" ? <Loader size="xs" /> : <EditNoteIcon fontSize="small" />}
|
||||
<span>Page Editor</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
value: "pageEditor",
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<Tooltip content="Active Files" position="bottom" arrow={true}>
|
||||
<div style={viewOptionStyle as React.CSSProperties}>
|
||||
{currentView === "fileEditor" ? (
|
||||
<>
|
||||
{switchingTo === "fileEditor" ? <Loader size="xs" /> : <FolderIcon fontSize="small" />}
|
||||
<span>Active Files</span>
|
||||
</>
|
||||
) : (
|
||||
switchingTo === "fileEditor" ? <Loader size="xs" /> : <FolderIcon fontSize="small" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div style={viewOptionStyle as React.CSSProperties}>
|
||||
{currentView === "fileEditor" ? (
|
||||
<>
|
||||
{switchingTo === "fileEditor" ? <Loader size="xs" /> : <FolderIcon fontSize="small" />}
|
||||
<span>Active Files</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{switchingTo === "fileEditor" ? <Loader size="xs" /> : <FolderIcon fontSize="small" />}
|
||||
<span>Active Files</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
value: "fileEditor",
|
||||
},
|
||||
|
||||
@@ -1,177 +1,155 @@
|
||||
# Tooltip Component
|
||||
|
||||
A flexible, accessible tooltip component that supports both regular positioning and special sidebar positioning logic with click-to-pin functionality. The tooltip is controlled by default, appearing on hover and pinning on click.
|
||||
A flexible, accessible tooltip component supporting regular positioning and special sidebar positioning, with optional click‑to‑pin behavior. By default, it opens on hover/focus and can be pinned on click when `pinOnClick` is enabled.
|
||||
|
||||
## Features
|
||||
---
|
||||
|
||||
- 🎯 **Smart Positioning**: Automatically positions tooltips to stay within viewport bounds
|
||||
- 📱 **Sidebar Support**: Special positioning logic for sidebar/navigation elements
|
||||
- ♿ **Accessible**: Works with mouse interactions and click-to-pin functionality
|
||||
- 🎨 **Customizable**: Support for arrows, structured content, and custom JSX
|
||||
- 🌙 **Theme Support**: Built-in dark mode and theme variable support
|
||||
- ⚡ **Performance**: Memoized calculations and efficient event handling
|
||||
- 📜 **Scrollable**: Content area scrolls when content exceeds max height
|
||||
- 📌 **Click-to-Pin**: Click to pin tooltips open, click outside or the close button to unpin
|
||||
- 🔗 **Link Support**: Full support for clickable links in descriptions, bullets, and body content
|
||||
- 🎮 **Controlled by Default**: Always uses controlled state management for consistent behavior
|
||||
- ⏱️ **Hover Timing Controls**: Optional long-hover requirement via `delayAppearance` and `delay`
|
||||
## Highlights
|
||||
|
||||
* 🎯 **Smart Positioning**: Keeps tooltips within the viewport and aligns the arrow dynamically.
|
||||
* 📱 **Sidebar Aware**: Purpose‑built logic for sidebar/navigation contexts.
|
||||
* ♿ **Accessible**: Keyboard and screen‑reader friendly (`role="tooltip"`, `aria-describedby`, Escape to close, focus/blur support).
|
||||
* 🎨 **Customizable**: Arrows, headers, rich JSX content, and structured tips.
|
||||
* 🌙 **Themeable**: Uses CSS variables; supports dark mode out of the box.
|
||||
* ⚡ **Efficient**: Memoized calculations and stable callbacks to minimize re‑renders.
|
||||
* 📜 **Scrollable Content**: When content exceeds max height.
|
||||
* 📌 **Click‑to‑Pin**: (Optional) Pin open; close via outside click or close button.
|
||||
* 🔗 **Link‑Safe**: Fully clickable links in descriptions, bullets, and custom content.
|
||||
* 🖱️ **Pointer‑Friendly**: Uses pointer events (works with mouse/pen/touch hover where applicable).
|
||||
|
||||
---
|
||||
|
||||
## Behavior
|
||||
|
||||
### Default Behavior (Controlled)
|
||||
- **Hover**: Tooltips appear on hover with a small delay when leaving to prevent flickering
|
||||
- **Click**: Click the trigger to pin the tooltip open
|
||||
- **Click tooltip**: Pins the tooltip to keep it open
|
||||
- **Click close button**: Unpins and closes the tooltip (red X button in top-right when pinned)
|
||||
- **Click outside**: Unpins and closes the tooltip
|
||||
- **Visual indicator**: Pinned tooltips have a blue border and close button
|
||||
### Default
|
||||
|
||||
### Manual Control (Optional)
|
||||
- Use `open` and `onOpenChange` props for complete external control
|
||||
- Useful for complex state management or custom interaction patterns
|
||||
* **Hover/Focus**: Opens on pointer **enter** or when the trigger receives **focus** (respects optional `delay`).
|
||||
* **Leave/Blur**: Closes on pointer **leave** (from trigger *and* tooltip) or when the trigger/tooltip **blurs** to the page—unless pinned.
|
||||
* **Inside Tooltip**: Moving from trigger → tooltip keeps it open; moving out of both closes it (unless pinned).
|
||||
* **Escape**: Press **Esc** to close.
|
||||
|
||||
### Click‑to‑Pin (optional)
|
||||
|
||||
* Enable with `pinOnClick`.
|
||||
* **Click trigger** (or tooltip) to pin open.
|
||||
* **Click outside** **both** trigger and tooltip to close when pinned.
|
||||
* Use the close button (X) to unpin and close.
|
||||
|
||||
> **Note**: Outside‑click closing when **not** pinned is configurable via `closeOnOutside` (default `true`).
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
```tsx
|
||||
import { Tooltip } from '@/components/shared';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```tsx
|
||||
import { Tooltip } from '@/components/shared';
|
||||
|
||||
function MyComponent() {
|
||||
return (
|
||||
<Tooltip content="This is a helpful tooltip">
|
||||
<button>Hover me</button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `content` | `ReactNode` | - | Custom JSX content to display in the tooltip |
|
||||
| `tips` | `TooltipTip[]` | - | Structured content with title, description, bullets, and optional body |
|
||||
| `children` | `ReactElement` | **required** | Element that triggers the tooltip |
|
||||
| `sidebarTooltip` | `boolean` | `false` | Enables special sidebar positioning logic |
|
||||
| `position` | `'right' \| 'left' \| 'top' \| 'bottom'` | `'right'` | Tooltip position (ignored if `sidebarTooltip` is true) |
|
||||
| `offset` | `number` | `8` | Distance in pixels between trigger and tooltip |
|
||||
| `maxWidth` | `number \| string` | `280` | Maximum width constraint for the tooltip |
|
||||
| `open` | `boolean` | `undefined` | External open state (makes component fully controlled) |
|
||||
| `onOpenChange` | `(open: boolean) => void` | `undefined` | Callback for external control |
|
||||
| `arrow` | `boolean` | `false` | Shows a small triangular arrow pointing to the trigger element |
|
||||
| `portalTarget` | `HTMLElement` | `undefined` | DOM node to portal the tooltip into |
|
||||
| `header` | `{ title: string; logo?: ReactNode }` | - | Optional header with title and logo |
|
||||
| `delay` | `number` | `0` | Optional hover-open delay (ms). If omitted or 0, opens immediately |
|
||||
|
||||
### TooltipTip Interface
|
||||
|
||||
```typescript
|
||||
interface TooltipTip {
|
||||
title?: string; // Optional pill label
|
||||
description?: string; // Optional description text (supports HTML including <a> tags)
|
||||
bullets?: string[]; // Optional bullet points (supports HTML including <a> tags)
|
||||
body?: React.ReactNode; // Optional custom JSX for this tip
|
||||
}
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Default Behavior (Recommended)
|
||||
|
||||
```tsx
|
||||
// Simple tooltip with hover and click-to-pin
|
||||
<Tooltip content="This tooltip appears on hover and pins on click">
|
||||
<Tooltip content="This is a helpful tooltip">
|
||||
<button>Hover me</button>
|
||||
</Tooltip>
|
||||
```
|
||||
|
||||
// Structured content with tips
|
||||
<Tooltip
|
||||
tips={[
|
||||
{
|
||||
title: "OCR Mode",
|
||||
description: "Choose how to process text in your documents.",
|
||||
bullets: [
|
||||
"<strong>Auto</strong> skips pages that already contain text.",
|
||||
"<strong>Force</strong> re-processes every page.",
|
||||
"<strong>Strict</strong> stops if text is found.",
|
||||
"<a href='https://docs.example.com' target='_blank'>Learn more</a>"
|
||||
]
|
||||
}
|
||||
]}
|
||||
header={{
|
||||
title: "Basic Settings Overview",
|
||||
logo: <img src="/logo.svg" alt="Logo" />
|
||||
}}
|
||||
With structured tips and a header:
|
||||
|
||||
```tsx
|
||||
<Tooltip
|
||||
tips={[{
|
||||
title: 'OCR Mode',
|
||||
description: 'Choose how to process text in your documents.',
|
||||
bullets: [
|
||||
'<strong>Auto</strong> skips pages that already contain text.',
|
||||
'<strong>Force</strong> re-processes every page.',
|
||||
'<strong>Strict</strong> stops if text is found.',
|
||||
"<a href='https://docs.example.com' target='_blank' rel='noreferrer'>Learn more</a>",
|
||||
],
|
||||
}]}
|
||||
header={{ title: 'Basic Settings Overview', logo: <img src="/logo.svg" alt="Logo" /> }}
|
||||
>
|
||||
<button>Settings</button>
|
||||
</Tooltip>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
### `<Tooltip />` Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
| ---------------- | ---------------------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `children` | `ReactElement` | **required** | The trigger element. Receives ARIA and event handlers. |
|
||||
| `content` | `ReactNode` | `undefined` | Custom JSX content rendered below any `tips`. |
|
||||
| `tips` | `TooltipTip[]` | `undefined` | Structured content (title, description, bullets, optional body). |
|
||||
| `sidebarTooltip` | `boolean` | `false` | Enables special sidebar positioning logic (no arrow in sidebar mode). |
|
||||
| `position` | `'right' \| 'left' \| 'top' \| 'bottom'` | `'right'` | Preferred placement (ignored if `sidebarTooltip` is `true`). |
|
||||
| `offset` | `number` | `8` | Gap (px) between trigger and tooltip. |
|
||||
| `maxWidth` | `number \| string` | `undefined` | Max width. If omitted and `sidebarTooltip` is true, defaults visually to \~`25rem`. |
|
||||
| `minWidth` | `number \| string` | `undefined` | Min width. |
|
||||
| `open` | `boolean` | `undefined` | Controlled open state. If provided, the component is controlled. |
|
||||
| `onOpenChange` | `(open: boolean) => void` | `undefined` | Callback when open state would change. |
|
||||
| `arrow` | `boolean` | `false` | Shows a directional arrow (suppressed in sidebar mode). |
|
||||
| `portalTarget` | `HTMLElement` | `undefined` | DOM node to portal the tooltip into. |
|
||||
| `header` | `{ title: string; logo?: ReactNode }` | `undefined` | Optional header with title and logo. |
|
||||
| `delay` | `number` | `0` | Hover/focus open delay in ms. |
|
||||
| `containerStyle` | `React.CSSProperties` | `{}` | Inline style overrides for the tooltip container. |
|
||||
| `pinOnClick` | `boolean` | `false` | Clicking the trigger pins the tooltip open. |
|
||||
| `closeOnOutside` | `boolean` | `true` | When not pinned, clicking outside closes the tooltip. Always closes when pinned and clicking outside both trigger & tooltip. |
|
||||
|
||||
### `TooltipTip`
|
||||
|
||||
```ts
|
||||
export interface TooltipTip {
|
||||
title?: string; // Optional pill label
|
||||
description?: string; // HTML allowed (e.g., <a>)
|
||||
bullets?: string[]; // HTML allowed in each string
|
||||
body?: React.ReactNode; // Optional custom JSX
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Accessibility
|
||||
|
||||
* The tooltip container uses `role="tooltip"` and gets a stable `id`.
|
||||
* The trigger receives `aria-describedby` when the tooltip is open.
|
||||
* Opens on **focus** and closes on **blur** (unless pinned), supporting keyboard navigation.
|
||||
* **Escape** closes the tooltip.
|
||||
* Pointer events are mirrored with keyboard/focus for parity.
|
||||
|
||||
> Ensure custom triggers remain focusable (e.g., `button`, `a`, or add `tabIndex=0`).
|
||||
|
||||
---
|
||||
|
||||
## Interaction Details
|
||||
|
||||
* **Hover Timing**: Opening can be delayed via `delay`. Closing is immediate on pointer leave from both trigger and tooltip (unless pinned). Timers are cleared on state changes and unmounts.
|
||||
* **Outside Clicks**: When pinned, clicking outside **both** the trigger and tooltip closes it. When not pinned, outside clicks close it if `closeOnOutside` is `true`.
|
||||
* **Event Preservation**: Original child event handlers (`onClick`, `onPointerEnter`, etc.) are called after the tooltip augments them.
|
||||
* **Refs**: The trigger’s existing `ref` (function or object) is preserved.
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### With Arrow
|
||||
|
||||
```tsx
|
||||
<Tooltip content="Arrow tooltip" arrow position="top">
|
||||
<button>Arrow tooltip</button>
|
||||
</Tooltip>
|
||||
```
|
||||
|
||||
### Optional Hover Delay
|
||||
|
||||
```tsx
|
||||
// Show after a 1s hover
|
||||
<Tooltip content="Appears after a long hover" delay={1000} />
|
||||
|
||||
// Custom long-hover duration (2 seconds)
|
||||
<Tooltip content="Appears after 2s" delay={2000} />
|
||||
```
|
||||
|
||||
|
||||
### Custom JSX Content
|
||||
|
||||
```tsx
|
||||
<Tooltip
|
||||
content={
|
||||
<div>
|
||||
<h3>Custom Content</h3>
|
||||
<p>Any JSX you want here</p>
|
||||
<button>Action</button>
|
||||
<a href="https://example.com">External link</a>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<button>Custom tooltip</button>
|
||||
</Tooltip>
|
||||
```
|
||||
|
||||
### Mixed Content (Tips + Custom JSX)
|
||||
|
||||
```tsx
|
||||
<Tooltip
|
||||
tips={[
|
||||
{ title: "Section", description: "Description" }
|
||||
]}
|
||||
content={<div>Additional custom content below tips</div>}
|
||||
>
|
||||
<button>Mixed content</button>
|
||||
</Tooltip>
|
||||
```
|
||||
|
||||
### Sidebar Tooltips
|
||||
|
||||
```tsx
|
||||
// For items in a sidebar/navigation
|
||||
<Tooltip
|
||||
content="This tooltip appears to the right of the sidebar"
|
||||
sidebarTooltip={true}
|
||||
>
|
||||
<div className="sidebar-item">
|
||||
📁 File Manager
|
||||
</div>
|
||||
</Tooltip>
|
||||
```
|
||||
|
||||
### With Arrows
|
||||
|
||||
```tsx
|
||||
<Tooltip
|
||||
content="Tooltip with arrow pointing to trigger"
|
||||
arrow={true}
|
||||
position="top"
|
||||
>
|
||||
<button>Arrow tooltip</button>
|
||||
<Tooltip content="Appears after 1s" delay={1000}>
|
||||
<button>Delayed</button>
|
||||
</Tooltip>
|
||||
```
|
||||
|
||||
@@ -180,63 +158,55 @@ interface TooltipTip {
|
||||
```tsx
|
||||
function ManualControlTooltip() {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
content="Fully controlled tooltip"
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<button onClick={() => setOpen(!open)}>
|
||||
Toggle tooltip
|
||||
</button>
|
||||
<Tooltip content="Fully controlled tooltip" open={open} onOpenChange={setOpen}>
|
||||
<button onClick={() => setOpen(!open)}>Toggle tooltip</button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Click-to-Pin Interaction
|
||||
### Sidebar Tooltip
|
||||
|
||||
### How to Use (Default Behavior)
|
||||
1. **Hover** over the trigger element to show the tooltip
|
||||
2. **Click** the trigger element to pin the tooltip open
|
||||
3. **Click** the red X button in the top-right corner to close
|
||||
4. **Click** anywhere outside the tooltip to close
|
||||
5. **Click** the trigger again to toggle pin state
|
||||
```tsx
|
||||
<Tooltip content="Appears to the right of the sidebar" sidebarTooltip>
|
||||
<div className="sidebar-item">📁 File Manager</div>
|
||||
</Tooltip>
|
||||
```
|
||||
|
||||
### Visual States
|
||||
- **Unpinned**: Normal tooltip appearance
|
||||
- **Pinned**: Blue border, subtle glow, and close button (X) in top-right corner
|
||||
### Mixed Content
|
||||
|
||||
## Link Support
|
||||
```tsx
|
||||
<Tooltip
|
||||
tips={[{ title: 'Section', description: 'Description' }]}
|
||||
content={<div>Additional custom content below tips</div>}
|
||||
>
|
||||
<button>Mixed content</button>
|
||||
</Tooltip>
|
||||
```
|
||||
|
||||
The tooltip fully supports clickable links in all content areas:
|
||||
---
|
||||
|
||||
- **Descriptions**: Use `<a href="...">` in description strings
|
||||
- **Bullets**: Use `<a href="...">` in bullet point strings
|
||||
- **Body**: Use JSX `<a>` elements in the body ReactNode
|
||||
- **Content**: Use JSX `<a>` elements in custom content
|
||||
## Positioning Notes
|
||||
|
||||
Links automatically get proper styling with hover states and open in new tabs when using `target="_blank"`.
|
||||
* Initial placement is derived from `position` (or sidebar rules when `sidebarTooltip` is true).
|
||||
* Tooltip is clamped within the viewport; the arrow is offset to remain visually aligned with the trigger.
|
||||
* Sidebar mode positions to the sidebar’s edge and clamps vertically. Arrows are disabled in sidebar mode.
|
||||
|
||||
## Positioning Logic
|
||||
---
|
||||
|
||||
### Regular Tooltips
|
||||
- Uses the `position` prop to determine initial placement
|
||||
- Automatically clamps to viewport boundaries
|
||||
- Calculates optimal position based on trigger element's `getBoundingClientRect()`
|
||||
- **Dynamic arrow positioning**: Arrow stays aligned with trigger even when tooltip is clamped
|
||||
## Caveats & Tips
|
||||
|
||||
## Timing Details
|
||||
* Ensure your container doesn’t block pointer events between trigger and tooltip.
|
||||
* When using `portalTarget`, confirm it’s attached to `document.body` before rendering.
|
||||
* For very dynamic layouts, call positioning after layout changes (the hook already listens to open/refs/viewport).
|
||||
|
||||
- Opening uses `delay` (ms) if provided; otherwise opens immediately. Closing occurs immediately when the cursor leaves (unless pinned).
|
||||
- All internal timers are cleared on state changes, mouse transitions, and unmount to avoid overlaps.
|
||||
- Only one tooltip can be open at a time; hovering a new trigger closes others immediately.
|
||||
---
|
||||
|
||||
### Sidebar Tooltips
|
||||
- When `sidebarTooltip={true}`, horizontal positioning is locked to the right of the sidebar
|
||||
- Vertical positioning follows the trigger but clamps to viewport
|
||||
- **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
|
||||
## Changelog (since previous README)
|
||||
|
||||
* Added keyboard & ARIA details (focus/blur, Escape, `aria-describedby`).
|
||||
* Clarified outside‑click behavior for pinned vs unpinned.
|
||||
* Documented `closeOnOutside` and `minWidth`, `containerStyle`, `pinOnClick`.
|
||||
* Removed references to non‑existent props (e.g., `delayAppearance`).
|
||||
* Corrected defaults (no hard default `maxWidth`; sidebar visually \~`25rem`).
|
||||
|
||||
Reference in New Issue
Block a user