Compare commits

...

3 Commits

Author SHA1 Message Date
fdb3045053 Refactor Tooltip Timing and Enhance Dashboard Splitter Styles
- Reduced fade-out duration for tooltips from 500ms to 250ms and hide delay from 100ms to 10ms for improved responsiveness.
- Updated DashboardSidebarSplitter to include a new class for the content panel, enhancing layout flexibility and styling consistency.
- Adjusted CSS for cursor tooltip transitions to provide a smoother visual experience.
2026-08-20 22:00:31 +01:00
80ac635bf9 Enhance CursorTooltip with Visibility Control and CSS Transitions
- Updated the CursorTooltip component to accept a new `visible` prop, allowing for better control over tooltip visibility.
- Refactored tooltip rendering logic to utilize CSS transitions for smoother fade-in and fade-out effects, improving user experience.
- Enhanced TooltipContext to manage tooltip visibility state and timing, ensuring consistent behavior during interactions.
- Updated CSS styles for the cursor tooltip to support new visibility transitions, enhancing visual feedback.
2026-08-20 21:53:52 +01:00
ef57837371 Enhance Tooltip Component with Parent Listening Feature
- Updated the Tooltip component to include a new prop, `listenParents`, allowing it to listen for mouse events on parent elements, improving tooltip visibility and interaction.
- Refactored mouse event handling to manage hover states more effectively, ensuring tooltips behave consistently when interacting with parent elements.
- Improved code organization and readability by consolidating event handling logic and utilizing refs for better performance.
2026-08-20 21:46:19 +01:00
6 changed files with 157 additions and 53 deletions

View File

@ -361,6 +361,16 @@ code {
padding: 8px !important; padding: 8px !important;
} }
.cursor-tooltip {
opacity: 0;
transition: opacity 0.25s ease;
}
.cursor-tooltip.is-visible {
opacity: 1;
transition: opacity 0.15s ease;
}
.cursor-tooltip .ant-card { .cursor-tooltip .ant-card {
box-shadow: box-shadow:
0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 6px 16px 0 rgba(0, 0, 0, 0.08),
@ -1548,3 +1558,8 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
.ant-splitter-bar-dragger::after { .ant-splitter-bar-dragger::after {
content: none; content: none;
} }
.dashboard-splitter.farmcontrol-splitter
.ant-splitter-panel.dashboard-splitter-content-panel {
flex-grow: 1 !important;
}

View File

@ -15,7 +15,7 @@ const getTooltipRoot = () => {
return root return root
} }
const CursorTooltip = ({ content, pointRef }) => { const CursorTooltip = ({ content, visible, pointRef }) => {
const tooltipRef = useRef(null) const tooltipRef = useRef(null)
const sizeRef = useRef({ width: 0, height: 0 }) const sizeRef = useRef({ width: 0, height: 0 })
const frameRef = useRef(null) const frameRef = useRef(null)
@ -94,23 +94,19 @@ const CursorTooltip = ({ content, pointRef }) => {
} }
}, [applyPosition, root, schedulePosition]) }, [applyPosition, root, schedulePosition])
const visible = content != null && content !== false && content !== ''
if (!root) return null if (!root) return null
return createPortal( return createPortal(
<div <div
ref={tooltipRef} ref={tooltipRef}
className='cursor-tooltip' className={`cursor-tooltip${visible ? ' is-visible' : ''}`}
style={{ style={{
position: 'fixed', position: 'fixed',
top: 0, top: 0,
left: 0, left: 0,
zIndex: 1100, zIndex: 1100,
pointerEvents: 'none', pointerEvents: 'none',
visibility: visible ? 'visible' : 'hidden', willChange: 'transform, opacity'
opacity: visible ? 1 : 0,
willChange: 'transform'
}} }}
> >
<Card <Card
@ -131,6 +127,7 @@ const CursorTooltip = ({ content, pointRef }) => {
CursorTooltip.propTypes = { CursorTooltip.propTypes = {
content: PropTypes.node, content: PropTypes.node,
visible: PropTypes.bool,
pointRef: PropTypes.shape({ pointRef: PropTypes.shape({
current: PropTypes.shape({ current: PropTypes.shape({
x: PropTypes.number, x: PropTypes.number,

View File

@ -1,4 +1,10 @@
import { cloneElement, isValidElement, useState, useEffect, useContext } from 'react' import {
cloneElement,
isValidElement,
useState,
useEffect,
useContext
} from 'react'
import { Layout, Menu, Flex, Button } from 'antd' import { Layout, Menu, Flex, Button } from 'antd'
import { CaretDownFilled } from '@ant-design/icons' import { CaretDownFilled } from '@ant-design/icons'
import CollapseSidebarIcon from '../../Icons/CollapseSidebarIcon' import CollapseSidebarIcon from '../../Icons/CollapseSidebarIcon'
@ -23,7 +29,7 @@ const CollapsedItemIcon = ({ className, icon, title }) => {
: icon : icon
return ( return (
<Tooltip title={title}> <Tooltip title={title} listenParents={1}>
{iconNode ?? <span className={className} />} {iconNode ?? <span className={className} />}
</Tooltip> </Tooltip>
) )

View File

@ -208,7 +208,9 @@ const DashboardSidebarSplitter = ({ sidebar, children }) => {
> >
{sidebarNode} {sidebarNode}
</Splitter.Panel> </Splitter.Panel>
<Splitter.Panel min={320}>{children}</Splitter.Panel> <Splitter.Panel min={320} className='dashboard-splitter-content-panel'>
{children}
</Splitter.Panel>
</Splitter> </Splitter>
</Layout> </Layout>
) )

View File

@ -13,11 +13,15 @@ const mergeHandler = (original, next) => (event) => {
original?.(event) original?.(event)
} }
const Tooltip = ({ children, title, content }) => { const Tooltip = ({ children, title, content, listenParents = 0 }) => {
const { showTooltip, hideTooltip } = useTooltipContext() const { showTooltip, hideTooltip } = useTooltipContext()
const id = useId() const id = useId()
const tooltipContent = title ?? content const tooltipContent = title ?? content
const spanRef = useRef(null)
const hoveredTargetsRef = useRef(new Set())
const hoveringRef = useRef(false) const hoveringRef = useRef(false)
const tooltipContentRef = useRef(tooltipContent)
tooltipContentRef.current = tooltipContent
useEffect(() => { useEffect(() => {
return () => hideTooltip(id) return () => hideTooltip(id)
@ -30,43 +34,86 @@ const Tooltip = ({ children, title, content }) => {
}, [id, showTooltip, tooltipContent]) }, [id, showTooltip, tooltipContent])
const onMouseEnter = (event) => { const onMouseEnter = (event) => {
hoveredTargetsRef.current.add(event.currentTarget)
hoveringRef.current = true hoveringRef.current = true
showTooltip(id, tooltipContent, event.clientX, event.clientY) showTooltip(id, tooltipContentRef.current, event.clientX, event.clientY)
} }
const onMouseLeave = () => { const onMouseLeave = (event) => {
hoveredTargetsRef.current.delete(event.currentTarget)
if (hoveredTargetsRef.current.size > 0) return
hoveringRef.current = false hoveringRef.current = false
hideTooltip(id) hideTooltip(id)
} }
const onMouseEnterRef = useRef(onMouseEnter)
const onMouseLeaveRef = useRef(onMouseLeave)
onMouseEnterRef.current = onMouseEnter
onMouseLeaveRef.current = onMouseLeave
useEffect(() => {
const span = spanRef.current
if (!span || listenParents < 1) return
const handleEnter = (event) => onMouseEnterRef.current(event)
const handleLeave = (event) => onMouseLeaveRef.current(event)
const parents = []
let current = span.parentElement
for (let i = 0; i < listenParents && current; i += 1) {
parents.push(current)
current = current.parentElement
}
parents.forEach((parent) => {
parent.addEventListener('mouseenter', handleEnter)
parent.addEventListener('mouseleave', handleLeave)
})
return () => {
parents.forEach((parent) => {
parent.removeEventListener('mouseenter', handleEnter)
parent.removeEventListener('mouseleave', handleLeave)
})
hoveredTargetsRef.current.clear()
}
}, [listenParents])
const wrapWithSpan = (node) => (
<span
ref={spanRef}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
{node}
</span>
)
if (isValidElement(children)) { if (isValidElement(children)) {
const trigger = cloneElement(children, { const trigger = cloneElement(children, {
onMouseEnter: mergeHandler(children.props.onMouseEnter, onMouseEnter), onMouseEnter: mergeHandler(children.props.onMouseEnter, onMouseEnter),
onMouseLeave: mergeHandler(children.props.onMouseLeave, onMouseLeave) onMouseLeave: mergeHandler(children.props.onMouseLeave, onMouseLeave)
}) })
if (listenParents > 0) {
return wrapWithSpan(children)
}
if (children.props.disabled) { if (children.props.disabled) {
return ( return wrapWithSpan(trigger)
<span onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
{trigger}
</span>
)
} }
return trigger return trigger
} }
return ( return wrapWithSpan(children)
<span onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
{children}
</span>
)
} }
Tooltip.propTypes = { Tooltip.propTypes = {
children: PropTypes.node.isRequired, children: PropTypes.node.isRequired,
title: PropTypes.node, title: PropTypes.node,
content: PropTypes.node content: PropTypes.node,
listenParents: PropTypes.number
} }
export default Tooltip export default Tooltip

View File

@ -11,11 +11,31 @@ import CursorTooltip from '../common/CursorTooltip'
const TooltipContext = createContext(null) const TooltipContext = createContext(null)
const HIDE_DELAY_MS = 10
const FADE_OUT_MS = 250
export const TooltipProvider = ({ children }) => { export const TooltipProvider = ({ children }) => {
const [content, setContent] = useState(null) const [content, setContent] = useState(null)
const [visible, setVisible] = useState(false)
const stackRef = useRef([]) const stackRef = useRef([])
const hideTimerRef = useRef(null) const hideTimerRef = useRef(null)
const fadeTimerRef = useRef(null)
const pointRef = useRef({ x: 0, y: 0 }) const pointRef = useRef({ x: 0, y: 0 })
const visibleRef = useRef(false)
visibleRef.current = visible
const clearTimers = useCallback(() => {
if (hideTimerRef.current != null) {
window.clearTimeout(hideTimerRef.current)
hideTimerRef.current = null
}
if (fadeTimerRef.current != null) {
window.clearTimeout(fadeTimerRef.current)
fadeTimerRef.current = null
}
}, [])
useEffect(() => () => clearTimers(), [clearTimers])
useEffect(() => { useEffect(() => {
const onMove = (event) => { const onMove = (event) => {
@ -25,7 +45,8 @@ export const TooltipProvider = ({ children }) => {
return () => window.removeEventListener('mousemove', onMove) return () => window.removeEventListener('mousemove', onMove)
}, []) }, [])
const showTooltip = useCallback((id, nextContent, x, y) => { const showTooltip = useCallback(
(id, nextContent, x, y) => {
if (nextContent == null || nextContent === false || nextContent === '') { if (nextContent == null || nextContent === false || nextContent === '') {
return return
} }
@ -34,36 +55,52 @@ export const TooltipProvider = ({ children }) => {
pointRef.current = { x, y } pointRef.current = { x, y }
} }
if (hideTimerRef.current != null) { clearTimers()
window.clearTimeout(hideTimerRef.current)
hideTimerRef.current = null
}
stackRef.current = [ stackRef.current = [
...stackRef.current.filter((entry) => entry.id !== id), ...stackRef.current.filter((entry) => entry.id !== id),
{ id, content: nextContent } { id, content: nextContent }
] ]
setContent(nextContent) setContent(nextContent)
}, [])
const hideTooltip = useCallback((id) => { if (!visibleRef.current) {
window.requestAnimationFrame(() => setVisible(true))
} else {
setVisible(true)
}
},
[clearTimers]
)
const hideTooltip = useCallback(
(id) => {
stackRef.current = stackRef.current.filter((entry) => entry.id !== id) stackRef.current = stackRef.current.filter((entry) => entry.id !== id)
const last = stackRef.current[stackRef.current.length - 1] const last = stackRef.current[stackRef.current.length - 1]
if (hideTimerRef.current != null) { clearTimers()
window.clearTimeout(hideTimerRef.current)
if (last) {
setContent(last.content)
setVisible(true)
return
} }
hideTimerRef.current = window.setTimeout(() => { hideTimerRef.current = window.setTimeout(() => {
hideTimerRef.current = null hideTimerRef.current = null
setContent(last?.content ?? null) setVisible(false)
}, 80) fadeTimerRef.current = window.setTimeout(() => {
}, []) fadeTimerRef.current = null
setContent(null)
}, FADE_OUT_MS)
}, HIDE_DELAY_MS)
},
[clearTimers]
)
return ( return (
<TooltipContext.Provider value={{ showTooltip, hideTooltip }}> <TooltipContext.Provider value={{ showTooltip, hideTooltip }}>
{children} {children}
<CursorTooltip content={content} pointRef={pointRef} /> <CursorTooltip content={content} visible={visible} pointRef={pointRef} />
</TooltipContext.Provider> </TooltipContext.Provider>
) )
} }