Implement Tooltip System and Enhance UI Components

- Introduced a new Tooltip component to standardize tooltip functionality across the application, improving consistency and usability.
- Refactored existing components to utilize the new Tooltip implementation, enhancing user interactions with clearer feedback.
- Added a CursorTooltip for dynamic tooltip positioning based on mouse movement, improving user experience.
- Updated CSS styles for tooltips and related components to enhance visual clarity and responsiveness.
- Integrated TooltipProvider to manage tooltip state and visibility, streamlining tooltip management across the application.
This commit is contained in:
Tom Butcher 2026-08-20 20:14:51 +01:00
parent 0a09905f9f
commit 8ac04ac0bc
16 changed files with 419 additions and 88 deletions

View File

@ -361,6 +361,11 @@ code {
padding: 8px !important;
}
.cursor-tooltip .ant-card {
box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08),
0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
}
/* --- Start of src/index.css --- */
body {
margin: 0;

View File

@ -30,6 +30,7 @@ import { ApiServerProvider } from './components/Dashboard/context/ApiServerConte
import { NotificationProvider } from './components/Dashboard/context/NotificationContext.jsx'
import { ElectronProvider } from './components/Dashboard/context/ElectronContext.jsx'
import { MessageProvider } from './components/Dashboard/context/MessageContext.jsx'
import { TooltipProvider } from './components/Dashboard/context/TooltipContext.jsx'
import { AppUpdateProvider } from './components/Dashboard/context/AppUpdateContext.jsx'
import AuthCallback from './components/App/AuthCallback.jsx'
import EmailNotificationTemplate from './components/Email/EmailNotificationTemplate.jsx'
@ -85,13 +86,14 @@ const AppContent = () => {
<PrintServerProvider>
<ApiServerProvider>
<MessageProvider>
<TableStateProvider>
<AppUpdateProvider>
<NotificationProvider>
<SpotlightProvider>
<ActionsModalProvider>
<ActionsProvider>
<Routes>
<TooltipProvider>
<TableStateProvider>
<AppUpdateProvider>
<NotificationProvider>
<SpotlightProvider>
<ActionsModalProvider>
<ActionsProvider>
<Routes>
<Route
path='/applaunch'
element={<AuthLaunch />}
@ -162,12 +164,13 @@ const AppContent = () => {
}
/>
</Routes>
</ActionsProvider>
</ActionsModalProvider>
</SpotlightProvider>
</NotificationProvider>
</AppUpdateProvider>
</TableStateProvider>
</ActionsProvider>
</ActionsModalProvider>
</SpotlightProvider>
</NotificationProvider>
</AppUpdateProvider>
</TableStateProvider>
</TooltipProvider>
</MessageProvider>
</ApiServerProvider>
</PrintServerProvider>

View File

@ -1,6 +1,7 @@
import PropTypes from 'prop-types'
import { Button, Tooltip } from 'antd'
import { Button } from 'antd'
import { useMessageContext } from '../context/MessageContext'
import Tooltip from './Tooltip'
import CopyIcon from '../../Icons/CopyIcon'
const CopyButton = ({
@ -49,7 +50,7 @@ const CopyButton = ({
return (
<>
<Tooltip title={tooltip} arrow={false}>
<Tooltip title={tooltip}>
<Button
icon={<CopyIcon />}
style={{ minWidth: 25 }}

View File

@ -0,0 +1,142 @@
import { useCallback, useLayoutEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { Card } from 'antd'
import PropTypes from 'prop-types'
const OFFSET = 10
const getTooltipRoot = () => {
let root = document.getElementById('cursor-tooltip-root')
if (!root) {
root = document.createElement('div')
root.id = 'cursor-tooltip-root'
document.documentElement.appendChild(root)
}
return root
}
const CursorTooltip = ({ content, pointRef }) => {
const tooltipRef = useRef(null)
const sizeRef = useRef({ width: 0, height: 0 })
const frameRef = useRef(null)
const contentRef = useRef(content)
const lastContentRef = useRef(content)
const [root, setRoot] = useState(null)
contentRef.current = content
if (content != null && content !== false && content !== '') {
lastContentRef.current = content
}
const applyPosition = useCallback(() => {
const el = tooltipRef.current
if (!el) return
const { width, height } = sizeRef.current
const { x, y } = pointRef.current
let left = x + OFFSET
let top = y + OFFSET
const maxLeft = window.innerWidth - width - OFFSET
const maxTop = window.innerHeight - height - OFFSET
if (width > 0 && left > maxLeft) left = Math.max(OFFSET, maxLeft)
if (height > 0 && top > maxTop) top = Math.max(OFFSET, maxTop)
el.style.transform = `translate3d(${left}px, ${top}px, 0)`
}, [pointRef])
const schedulePosition = useCallback(() => {
if (frameRef.current != null) return
frameRef.current = window.requestAnimationFrame(() => {
frameRef.current = null
applyPosition()
})
}, [applyPosition])
useLayoutEffect(() => {
setRoot(getTooltipRoot())
}, [])
useLayoutEffect(() => {
applyPosition()
}, [applyPosition, content])
useLayoutEffect(() => {
const el = tooltipRef.current
if (!el) return
const updateSize = () => {
sizeRef.current = {
width: el.offsetWidth,
height: el.offsetHeight
}
applyPosition()
}
updateSize()
const observer = new ResizeObserver(updateSize)
observer.observe(el)
const onMove = () => {
const next = contentRef.current
if (next == null || next === false || next === '') return
schedulePosition()
}
window.addEventListener('mousemove', onMove, { passive: true })
return () => {
observer.disconnect()
window.removeEventListener('mousemove', onMove)
if (frameRef.current != null) {
window.cancelAnimationFrame(frameRef.current)
frameRef.current = null
}
}
}, [applyPosition, root, schedulePosition])
const visible = content != null && content !== false && content !== ''
if (!root) return null
return createPortal(
<div
ref={tooltipRef}
className='cursor-tooltip'
style={{
position: 'fixed',
top: 0,
left: 0,
zIndex: 1100,
pointerEvents: 'none',
visibility: visible ? 'visible' : 'hidden',
opacity: visible ? 1 : 0,
willChange: 'transform'
}}
>
<Card
size='small'
styles={{ body: { padding: '4px 10px' } }}
style={{
maxWidth: 360,
borderRadius: 10,
backgroundColor: 'var(--layout-modal-bg)'
}}
>
{lastContentRef.current}
</Card>
</div>,
root
)
}
CursorTooltip.propTypes = {
content: PropTypes.node,
pointRef: PropTypes.shape({
current: PropTypes.shape({
x: PropTypes.number,
y: PropTypes.number
})
}).isRequired
}
export default CursorTooltip

View File

@ -6,7 +6,6 @@ import {
Tag,
Space,
Button,
Tooltip,
Badge,
Divider,
Typography,
@ -17,6 +16,7 @@ import { AuthContext } from '../context/AuthContext'
import { SpotlightContext } from '../context/SpotlightContext'
import { ApiServerContext } from '../context/ApiServerContext'
import { NotificationContext } from '../context/NotificationContext'
import Tooltip from './Tooltip'
import { useNavigate, useLocation } from 'react-router-dom'
import { Header } from 'antd/es/layout/layout'
import { useMediaQuery } from 'react-responsive'
@ -43,7 +43,8 @@ import WindowAppMenu from './WindowAppMenu'
import WebAppSwitcher from './WebAppSwitcher'
import {
getSidebarDefaultPath,
getSidebarMenuSections
getSidebarMenuSections,
isSidebarSectionVisible
} from '../../../database/Sidebars'
import { useAppUpdateContext } from '../context/AppUpdateContext'
@ -75,40 +76,44 @@ const DashboardNavigation = () => {
isMaximized
} = useContext(ElectronContext)
const { availableUpdate, checkForUpdates } = useAppUpdateContext()
const includeDev = import.meta.env.DEV
const mainMenuItems = useMemo(
() => [
{
key: 'production',
label: 'Production',
className: 'electrobun-webkit-app-region-no-drag',
icon: <ProductionIcon />
},
{
key: 'inventory',
label: 'Inventory',
className: 'electrobun-webkit-app-region-no-drag',
icon: <InventoryIcon />
},
{
key: 'sales',
label: 'Sales',
className: 'electrobun-webkit-app-region-no-drag',
icon: <SalesIcon />
},
{
key: 'finance',
label: 'Finance',
className: 'electrobun-webkit-app-region-no-drag',
icon: <FinanceIcon />
},
{
key: 'management',
label: 'Management',
className: 'electrobun-webkit-app-region-no-drag',
icon: <SettingsIcon />
}
],
[]
() =>
[
{
key: 'production',
label: 'Production',
className: 'electrobun-webkit-app-region-no-drag',
icon: <ProductionIcon />
},
{
key: 'inventory',
label: 'Inventory',
className: 'electrobun-webkit-app-region-no-drag',
icon: <InventoryIcon />
},
{
key: 'sales',
label: 'Sales',
className: 'electrobun-webkit-app-region-no-drag',
icon: <SalesIcon />
},
{
key: 'finance',
label: 'Finance',
className: 'electrobun-webkit-app-region-no-drag',
icon: <FinanceIcon />
},
{
key: 'management',
label: 'Management',
className: 'electrobun-webkit-app-region-no-drag',
icon: <SettingsIcon />
}
].filter((item) =>
isSidebarSectionVisible(item.key, { includeDev, userProfile })
),
[includeDev, userProfile]
)
const [userPopoverOpen, setUserPopoverOpen] = useState(false)
@ -120,10 +125,13 @@ const DashboardNavigation = () => {
useEffect(() => {
const pathParts = location.pathname.split('/').filter(Boolean)
if (pathParts.length > 2) {
setSelectedMenuItem(
mainMenuItems.filter((item) => item.key == pathParts[1])[0]
)
setSelectedKey(pathParts[1]) // Return the section (production/management)
const nextItem =
mainMenuItems.find((item) => item.key == pathParts[1]) ||
mainMenuItems[0]
if (nextItem) {
setSelectedMenuItem(nextItem)
setSelectedKey(nextItem.key)
}
}
}, [location.pathname, mainMenuItems])
@ -138,15 +146,14 @@ const DashboardNavigation = () => {
}, [connecting, connected])
const handleMainMenuClick = ({ key }) => {
navigate(getSidebarDefaultPath(key, { includeDev: import.meta.env.DEV }))
navigate(getSidebarDefaultPath(key, { includeDev, userProfile }))
}
useEffect(() => {
if (!isElectron || !setSidebarViewMenu) return
const includeDev = import.meta.env.DEV
const sections = getSidebarMenuSections({ includeDev })
const sections = getSidebarMenuSections({ includeDev, userProfile })
setSidebarViewMenu(sections)
}, [isElectron, setSidebarViewMenu])
}, [includeDev, isElectron, setSidebarViewMenu, userProfile])
const isMacOSApp = isElectron && platform == 'darwin'
const isOtherApp = isElectron && platform != 'darwin'
@ -194,7 +201,7 @@ const DashboardNavigation = () => {
maxWidth: '100%' // responsive
}}
>
{selectedMenuItem.icon}
{selectedMenuItem?.icon}
<ElipsisText
style={{
minWidth: 0,
@ -204,7 +211,7 @@ const DashboardNavigation = () => {
flex: 1
}}
>
{selectedMenuItem.label}
{selectedMenuItem?.label}
</ElipsisText>
</Flex>
)}
@ -292,7 +299,7 @@ const DashboardNavigation = () => {
{import.meta.env.MODE === 'development' && (
<Space>
{apiServerState === 'connected' ? (
<Tooltip title='Connected to api server' arrow={false}>
<Tooltip content='Connected to api server'>
<Tag
color='success'
style={{ marginRight: 0 }}
@ -301,7 +308,7 @@ const DashboardNavigation = () => {
</Tooltip>
) : null}
{apiServerState === 'connecting' ? (
<Tooltip title='Connecting to api erver...' arrow={false}>
<Tooltip content='Connecting to api erver...'>
<Tag
color='warning'
style={{ marginRight: 0 }}
@ -310,7 +317,7 @@ const DashboardNavigation = () => {
</Tooltip>
) : null}
{apiServerState === 'disconnected' ? (
<Tooltip title='Disconnected from api server' arrow={false}>
<Tooltip content='Disconnected from api server'>
<Tag
color='error'
style={{ marginRight: 0 }}
@ -318,7 +325,7 @@ const DashboardNavigation = () => {
/>
</Tooltip>
) : null}
<Tooltip title='Developer' arrow={false}>
<Tooltip content='Developer'>
<Tag
color='yellow'
style={{ marginRight: 0 }}

View File

@ -7,9 +7,11 @@ import { useMediaQuery } from 'react-responsive'
import { useNavigate } from 'react-router-dom'
import PropTypes from 'prop-types'
import { ElectronContext } from '../context/ElectronContext'
import { AuthContext } from '../context/AuthContext'
import { getSidebarIconNode } from '../../Icons/sidebarIconMap'
import SimpleBar from 'simplebar-react'
import { useThemeContext } from '../context/ThemeContext'
import { filterSidebarItemsByListPermission } from '../../../database/Sidebars'
const { Sider } = Layout
const DashboardSidebar = ({
@ -28,6 +30,8 @@ const DashboardSidebar = ({
const navigate = useNavigate()
const { isDarkMode } = useThemeContext()
const { isElectron } = useContext(ElectronContext)
const { userProfile } = useContext(AuthContext)
const allowedItems = filterSidebarItemsByListPermission(items, userProfile)
useEffect(() => {
if (typeof collapsedProp === 'boolean') {
@ -69,7 +73,7 @@ const DashboardSidebar = ({
}
// Map items recursively
const _items = mapItemsRecursively(items)
const _items = mapItemsRecursively(allowedItems)
if (isMobile) {
return (

View File

@ -1,5 +1,6 @@
import PropTypes from 'prop-types'
import { Typography, Flex, Button, Tooltip } from 'antd'
import { Typography, Flex, Button } from 'antd'
import Tooltip from './Tooltip'
import NewMailIcon from '../../Icons/NewMailIcon'
// import CopyIcon from './CopyIcon'
import CopyButton from './CopyButton'
@ -22,7 +23,7 @@ const EmailDisplay = ({ email, showCopy = true, showLink = false }) => {
<ElipsisText style={{ marginRight: 8 }}>
{email}
</ElipsisText>
<Tooltip title='Email' arrow={false}>
<Tooltip title='Email'>
<Button
icon={<NewMailIcon />}
type='text'

View File

@ -9,10 +9,10 @@ import {
Input,
Popover,
Select,
Space,
Tooltip
Space
} from 'antd'
import PropTypes from 'prop-types'
import Tooltip from './Tooltip'
import BlockquoteIcon from '../../Icons/BlockquoteIcon'
import BoldIcon from '../../Icons/BoldIcon'
import BulletListIcon from '../../Icons/BulletListIcon'
@ -199,7 +199,7 @@ const MarkdownToolbar = ({
disabled={!editor || editingToolsDisabled}
/>
<Space.Compact size='small'>
<Tooltip title='Bold' arrow={false}>
<Tooltip content='Bold'>
<Button
size={size}
type={editorState.isBold ? 'primary' : 'default'}
@ -209,7 +209,7 @@ const MarkdownToolbar = ({
style={{ minWidth: 32 }}
/>
</Tooltip>
<Tooltip title='Italic' arrow={false}>
<Tooltip content='Italic'>
<Button
size={size}
type={editorState.isItalic ? 'primary' : 'default'}
@ -219,7 +219,7 @@ const MarkdownToolbar = ({
style={{ minWidth: 32 }}
/>
</Tooltip>
<Tooltip title='Underline' arrow={false}>
<Tooltip content='Underline'>
<Button
size={size}
type={editorState.isUnderline ? 'primary' : 'default'}
@ -231,7 +231,7 @@ const MarkdownToolbar = ({
</Tooltip>
</Space.Compact>
<Space.Compact size='small'>
<Tooltip title='Bulleted list' arrow={false}>
<Tooltip content='Bulleted list'>
<Button
size={size}
type={editorState.isBulletList ? 'primary' : 'default'}
@ -241,7 +241,7 @@ const MarkdownToolbar = ({
style={{ minWidth: 32 }}
/>
</Tooltip>
<Tooltip title='Numbered list' arrow={false}>
<Tooltip content='Numbered list'>
<Button
size={size}
type={editorState.isOrderedList ? 'primary' : 'default'}
@ -252,7 +252,7 @@ const MarkdownToolbar = ({
/>
</Tooltip>
</Space.Compact>
<Tooltip title='Block quote' arrow={false}>
<Tooltip content='Block quote'>
<Button
size={size}
type={editorState.blockTypeValue === 'quote' ? 'primary' : 'default'}
@ -275,7 +275,7 @@ const MarkdownToolbar = ({
styles={{ body: { borderRadius: '22px' } }}
onOpenChange={handleLinkPopoverOpenChange}
>
<Tooltip title='Insert link' arrow={false}>
<Tooltip content='Insert link'>
<Button
size={size}
icon={<LinkIcon />}
@ -284,7 +284,7 @@ const MarkdownToolbar = ({
/>
</Tooltip>
</Popover>
<Tooltip title='Insert code block' arrow={false}>
<Tooltip content='Insert code block'>
<Button
size={size}
icon={<JsonObjectIcon />}
@ -293,7 +293,7 @@ const MarkdownToolbar = ({
style={{ minWidth: 32 }}
/>
</Tooltip>
<Tooltip title='Insert horizontal rule' arrow={false}>
<Tooltip content='Insert horizontal rule'>
<Button
size={size}
icon={<MinusOutlined />}

View File

@ -19,7 +19,6 @@ import {
Spin,
Button,
Space,
Tooltip,
Form,
Splitter,
Card
@ -56,6 +55,7 @@ import {
useTableStatePersistence
} from '../context/TableStateContext'
import { hasActionPermission } from '../../../database/permissions'
import Tooltip from './Tooltip'
const logger = loglevel.getLogger('DasboardTable')
logger.setLevel(config.logLevel)
@ -448,7 +448,7 @@ const ObjectTable = forwardRef(
}
}
return (
<Tooltip key={index} title={action.label} arrow={false}>
<Tooltip key={index} title={action.label}>
<Button
icon={
action.icon ? (

View File

@ -9,7 +9,8 @@ import {
useLayoutEffect
} from 'react'
import PropTypes from 'prop-types'
import { Table, Flex, Tooltip, Button } from 'antd'
import { Table, Flex, Button } from 'antd'
import Tooltip from './Tooltip'
import { useMediaQuery } from 'react-responsive'
import { ElectronContext } from '../context/ElectronContext'
import {
@ -540,7 +541,7 @@ const PermissionsMatrix = ({
const indeterminate = inherit && state === null
return (
<Tooltip title={getCheckboxTitle(state, inherit)} arrow={false}>
<Tooltip title={getCheckboxTitle(state, inherit)}>
<span>
<PermissionCheckbox
checked={checked}

View File

@ -1,6 +1,7 @@
import { useState } from 'react'
import PropTypes from 'prop-types'
import { Typography, Tooltip, Button } from 'antd'
import { Typography, Button } from 'antd'
import Tooltip from './Tooltip'
import CopyButton from './CopyButton'
import EyeIcon from '../../Icons/EyeIcon'
import EyeSlashIcon from '../../Icons/EyeSlashIcon'
@ -20,7 +21,7 @@ const SecretDisplay = ({ value, reveal = false }) => {
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
<Text code>{reveal && visible ? value : masked}</Text>
{reveal && (
<Tooltip title={visible ? 'Hide' : 'Show'} arrow={false}>
<Tooltip title={visible ? 'Hide' : 'Show'}>
<Button
type='text'
icon={visible ? <EyeSlashIcon /> : <EyeIcon />}

View File

@ -0,0 +1,72 @@
import {
cloneElement,
isValidElement,
useEffect,
useId,
useRef
} from 'react'
import PropTypes from 'prop-types'
import { useTooltipContext } from '../context/TooltipContext'
const mergeHandler = (original, next) => (event) => {
next(event)
original?.(event)
}
const Tooltip = ({ children, title, content }) => {
const { showTooltip, hideTooltip } = useTooltipContext()
const id = useId()
const tooltipContent = title ?? content
const hoveringRef = useRef(false)
useEffect(() => {
return () => hideTooltip(id)
}, [hideTooltip, id])
useEffect(() => {
if (hoveringRef.current) {
showTooltip(id, tooltipContent)
}
}, [id, showTooltip, tooltipContent])
const onMouseEnter = (event) => {
hoveringRef.current = true
showTooltip(id, tooltipContent, event.clientX, event.clientY)
}
const onMouseLeave = () => {
hoveringRef.current = false
hideTooltip(id)
}
if (isValidElement(children)) {
const trigger = cloneElement(children, {
onMouseEnter: mergeHandler(children.props.onMouseEnter, onMouseEnter),
onMouseLeave: mergeHandler(children.props.onMouseLeave, onMouseLeave)
})
if (children.props.disabled) {
return (
<span onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
{trigger}
</span>
)
}
return trigger
}
return (
<span onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
{children}
</span>
)
}
Tooltip.propTypes = {
children: PropTypes.node.isRequired,
title: PropTypes.node,
content: PropTypes.node
}
export default Tooltip

View File

@ -1,5 +1,6 @@
import PropTypes from 'prop-types'
import { Typography, Flex, Button, Tooltip } from 'antd'
import { Typography, Flex, Button } from 'antd'
import Tooltip from './Tooltip'
import LinkIcon from '../../Icons/LinkIcon'
import CopyButton from './CopyButton'
import ElipsisText from './ElipsisText'
@ -34,7 +35,7 @@ const UrlDisplay = ({ url, showCopy = true, showLink = false }) => {
<ElipsisText style={{ marginRight: 8, minWidth: 0 }}>
{url}
</ElipsisText>
<Tooltip title='Open URL' arrow={false}>
<Tooltip title='Open URL'>
<Button
icon={<LinkIcon />}
type='text'

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types'
import { Button, Dropdown, Flex } from 'antd'
import { useNavigate } from 'react-router-dom'
import { ElectronContext } from '../context/ElectronContext'
import { AuthContext } from '../context/AuthContext'
import { useAppUpdateContext } from '../context/AppUpdateContext'
import { getSidebarMenuSections } from '../../../database/Sidebars'
import FarmControlLogoSmall from '../../Logos/FarmControlLogoSmall'
@ -75,13 +76,14 @@ MenuButton.propTypes = {
const WindowAppMenu = () => {
const navigate = useNavigate()
const { userProfile } = useContext(AuthContext)
const { handleWindowControl } = useContext(ElectronContext)
const { checkForUpdates } = useAppUpdateContext()
const includeDev = import.meta.env.DEV
const viewSections = useMemo(
() => getSidebarMenuSections({ includeDev }),
[includeDev]
() => getSidebarMenuSections({ includeDev, userProfile }),
[includeDev, userProfile]
)
const appMenuItems = useMemo(

View File

@ -120,6 +120,13 @@ const ActionsProvider = ({ children }) => {
}
if (action.type === 'page') {
if (action.name === 'list') {
lastHandledAction.current = actionKey
if (location.pathname !== model.url) {
navigate(model.url, { replace: true })
}
return
}
const pageName = action.pageName || action.name
const objectId =
getObjectIdFromSearch(actionObjectType, location.search) ||

View File

@ -0,0 +1,84 @@
import {
createContext,
useCallback,
useContext,
useEffect,
useRef,
useState
} from 'react'
import PropTypes from 'prop-types'
import CursorTooltip from '../common/CursorTooltip'
const TooltipContext = createContext(null)
export const TooltipProvider = ({ children }) => {
const [content, setContent] = useState(null)
const stackRef = useRef([])
const hideTimerRef = useRef(null)
const pointRef = useRef({ x: 0, y: 0 })
useEffect(() => {
const onMove = (event) => {
pointRef.current = { x: event.clientX, y: event.clientY }
}
window.addEventListener('mousemove', onMove, { passive: true })
return () => window.removeEventListener('mousemove', onMove)
}, [])
const showTooltip = useCallback((id, nextContent, x, y) => {
if (nextContent == null || nextContent === false || nextContent === '') {
return
}
if (typeof x === 'number' && typeof y === 'number') {
pointRef.current = { x, y }
}
if (hideTimerRef.current != null) {
window.clearTimeout(hideTimerRef.current)
hideTimerRef.current = null
}
stackRef.current = [
...stackRef.current.filter((entry) => entry.id !== id),
{ id, content: nextContent }
]
setContent(nextContent)
}, [])
const hideTooltip = useCallback((id) => {
stackRef.current = stackRef.current.filter((entry) => entry.id !== id)
const last = stackRef.current[stackRef.current.length - 1]
if (hideTimerRef.current != null) {
window.clearTimeout(hideTimerRef.current)
}
hideTimerRef.current = window.setTimeout(() => {
hideTimerRef.current = null
setContent(last?.content ?? null)
}, 80)
}, [])
return (
<TooltipContext.Provider value={{ showTooltip, hideTooltip }}>
{children}
<CursorTooltip content={content} pointRef={pointRef} />
</TooltipContext.Provider>
)
}
TooltipProvider.propTypes = {
children: PropTypes.node.isRequired
}
// eslint-disable-next-line react-refresh/only-export-components
export const useTooltipContext = () => {
const context = useContext(TooltipContext)
if (!context) {
throw new Error('useTooltipContext must be used within a TooltipProvider')
}
return context
}
export { TooltipContext }