Tom Butcher e1fbd6070a
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
Implement Keyboard Shortcuts and Tooltip Enhancements
- Added a new KeyboardKey component for consistent keyboard key styling across the application.
- Introduced KeyboardShortcut component to wrap buttons with keyboard shortcut hints, improving user accessibility.
- Enhanced Tooltip and CursorTooltip components to support a hideBorder prop, allowing for customizable tooltip appearances.
- Updated CSS styles for keyboard keys in light and dark modes, ensuring visual consistency and improved user experience.
2026-08-21 19:34:56 +01:00

127 lines
3.3 KiB
JavaScript

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,
listenParents = 0,
hideBorder = false
}) => {
const { showTooltip, hideTooltip } = useTooltipContext()
const id = useId()
const tooltipContent = title ?? content
const spanRef = useRef(null)
const hoveredTargetsRef = useRef(new Set())
const hoveringRef = useRef(false)
const tooltipContentRef = useRef(tooltipContent)
const hideBorderRef = useRef(hideBorder)
tooltipContentRef.current = tooltipContent
hideBorderRef.current = hideBorder
useEffect(() => {
return () => hideTooltip(id)
}, [hideTooltip, id])
useEffect(() => {
if (hoveringRef.current) {
showTooltip(id, tooltipContent, undefined, undefined, hideBorder)
}
}, [id, showTooltip, tooltipContent, hideBorder])
const onMouseEnter = (event) => {
hoveredTargetsRef.current.add(event.currentTarget)
hoveringRef.current = true
showTooltip(
id,
tooltipContentRef.current,
event.clientX,
event.clientY,
hideBorderRef.current
)
}
const onMouseLeave = (event) => {
hoveredTargetsRef.current.delete(event.currentTarget)
if (hoveredTargetsRef.current.size > 0) return
hoveringRef.current = false
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 hoveredTargets = hoveredTargetsRef.current
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)
})
hoveredTargets.clear()
}
}, [listenParents])
const wrapWithSpan = (node) => (
<span ref={spanRef} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
{node}
</span>
)
if (isValidElement(children)) {
const trigger = cloneElement(children, {
onMouseEnter: mergeHandler(children.props.onMouseEnter, onMouseEnter),
onMouseLeave: mergeHandler(children.props.onMouseLeave, onMouseLeave)
})
if (listenParents > 0) {
return wrapWithSpan(children)
}
if (children.props.disabled) {
return wrapWithSpan(trigger)
}
return trigger
}
return wrapWithSpan(children)
}
Tooltip.propTypes = {
children: PropTypes.node.isRequired,
title: PropTypes.node,
content: PropTypes.node,
listenParents: PropTypes.number,
hideBorder: PropTypes.bool
}
export default Tooltip