Implement Keyboard Shortcuts and Tooltip Enhancements
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- 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.
This commit is contained in:
parent
59bf2702bf
commit
e1fbd6070a
@ -357,8 +357,83 @@ code {
|
|||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-popover-inner:has(.keyboard-shortcut-tooltip) {
|
:root,
|
||||||
padding: 8px !important;
|
.light-mode,
|
||||||
|
html:has(.light-mode) {
|
||||||
|
--kbd-color: #363636;
|
||||||
|
--kbd-bg: #efefef;
|
||||||
|
--kbd-inset: #e8e8e8;
|
||||||
|
--kbd-edge: #c3c3c3;
|
||||||
|
--kbd-ridge: #c9c9c9;
|
||||||
|
--kbd-drop-shadow: #333333;
|
||||||
|
--kbd-text-shadow: #f6f6f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode,
|
||||||
|
html:has(.dark-mode) {
|
||||||
|
--kbd-color: #ececec;
|
||||||
|
--kbd-bg: #3a3a3a;
|
||||||
|
--kbd-inset: #323232;
|
||||||
|
--kbd-edge: #1c1c1c;
|
||||||
|
--kbd-ridge: #141414;
|
||||||
|
--kbd-drop-shadow: #000000;
|
||||||
|
--kbd-text-shadow: #111111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyboard-key {
|
||||||
|
display: inline-block;
|
||||||
|
color: var(--kbd-color);
|
||||||
|
text-decoration: none;
|
||||||
|
text-align: center;
|
||||||
|
background: var(--kbd-bg);
|
||||||
|
padding: 3px 7px;
|
||||||
|
margin: 5px 3px;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 25px var(--kbd-inset),
|
||||||
|
0 1px 0 var(--kbd-edge),
|
||||||
|
0 2px 0 var(--kbd-ridge),
|
||||||
|
0 2px 3px var(--kbd-drop-shadow);
|
||||||
|
text-shadow: 0 1px 0 var(--kbd-text-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyboard-key-md {
|
||||||
|
padding: 4.5px 10.5px;
|
||||||
|
margin: 7.5px 3.5px;
|
||||||
|
font-size: 110%;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 37.5px var(--kbd-inset),
|
||||||
|
0 1.5px 0 var(--kbd-edge),
|
||||||
|
0 3px 0 var(--kbd-ridge),
|
||||||
|
0 3px 4.5px var(--kbd-drop-shadow);
|
||||||
|
text-shadow: 0 1.5px 0 var(--kbd-text-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyboard-key-lg {
|
||||||
|
padding: 6px 14px;
|
||||||
|
margin: 10px 6px;
|
||||||
|
font-size: 200%;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 50px var(--kbd-inset),
|
||||||
|
0 2px 0 var(--kbd-edge),
|
||||||
|
0 4px 0 var(--kbd-ridge),
|
||||||
|
0 4px 6px var(--kbd-drop-shadow);
|
||||||
|
text-shadow: 0 2px 0 var(--kbd-text-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyboard-key-xl {
|
||||||
|
padding: 9px 21px;
|
||||||
|
margin: 15px 9px;
|
||||||
|
font-size: 300%;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 75px var(--kbd-inset),
|
||||||
|
0 3px 0 var(--kbd-edge),
|
||||||
|
0 6px 0 var(--kbd-ridge),
|
||||||
|
0 6px 9px var(--kbd-drop-shadow);
|
||||||
|
text-shadow: 0 3px 0 var(--kbd-text-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cursor-tooltip {
|
.cursor-tooltip {
|
||||||
|
|||||||
@ -15,17 +15,19 @@ const getTooltipRoot = () => {
|
|||||||
return root
|
return root
|
||||||
}
|
}
|
||||||
|
|
||||||
const CursorTooltip = ({ content, visible, pointRef }) => {
|
const CursorTooltip = ({ content, visible, pointRef, hideBorder = false }) => {
|
||||||
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)
|
||||||
const contentRef = useRef(content)
|
const contentRef = useRef(content)
|
||||||
const lastContentRef = useRef(content)
|
const lastContentRef = useRef(content)
|
||||||
|
const lastHideBorderRef = useRef(hideBorder)
|
||||||
const [root, setRoot] = useState(null)
|
const [root, setRoot] = useState(null)
|
||||||
|
|
||||||
contentRef.current = content
|
contentRef.current = content
|
||||||
if (content != null && content !== false && content !== '') {
|
if (content != null && content !== false && content !== '') {
|
||||||
lastContentRef.current = content
|
lastContentRef.current = content
|
||||||
|
lastHideBorderRef.current = hideBorder
|
||||||
}
|
}
|
||||||
|
|
||||||
const applyPosition = useCallback(() => {
|
const applyPosition = useCallback(() => {
|
||||||
@ -96,10 +98,28 @@ const CursorTooltip = ({ content, visible, pointRef }) => {
|
|||||||
|
|
||||||
if (!root) return null
|
if (!root) return null
|
||||||
|
|
||||||
|
const tooltipContent = lastHideBorderRef.current ? (
|
||||||
|
lastContentRef.current
|
||||||
|
) : (
|
||||||
|
<Card
|
||||||
|
size='small'
|
||||||
|
styles={{ body: { padding: '4px 10px' } }}
|
||||||
|
style={{
|
||||||
|
maxWidth: 360,
|
||||||
|
borderRadius: 10,
|
||||||
|
backgroundColor: 'var(--layout-modal-bg)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{lastContentRef.current}
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<div
|
<div
|
||||||
ref={tooltipRef}
|
ref={tooltipRef}
|
||||||
className={`cursor-tooltip${visible ? ' is-visible' : ''}`}
|
className={`cursor-tooltip${visible ? ' is-visible' : ''}${
|
||||||
|
lastHideBorderRef.current ? ' hide-border' : ''
|
||||||
|
}`}
|
||||||
style={{
|
style={{
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
top: 0,
|
top: 0,
|
||||||
@ -109,17 +129,7 @@ const CursorTooltip = ({ content, visible, pointRef }) => {
|
|||||||
willChange: 'transform, opacity'
|
willChange: 'transform, opacity'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Card
|
{tooltipContent}
|
||||||
size='small'
|
|
||||||
styles={{ body: { padding: '4px 10px' } }}
|
|
||||||
style={{
|
|
||||||
maxWidth: 360,
|
|
||||||
borderRadius: 10,
|
|
||||||
backgroundColor: 'var(--layout-modal-bg)'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{lastContentRef.current}
|
|
||||||
</Card>
|
|
||||||
</div>,
|
</div>,
|
||||||
root
|
root
|
||||||
)
|
)
|
||||||
@ -128,6 +138,7 @@ const CursorTooltip = ({ content, visible, pointRef }) => {
|
|||||||
CursorTooltip.propTypes = {
|
CursorTooltip.propTypes = {
|
||||||
content: PropTypes.node,
|
content: PropTypes.node,
|
||||||
visible: PropTypes.bool,
|
visible: PropTypes.bool,
|
||||||
|
hideBorder: PropTypes.bool,
|
||||||
pointRef: PropTypes.shape({
|
pointRef: PropTypes.shape({
|
||||||
current: PropTypes.shape({
|
current: PropTypes.shape({
|
||||||
x: PropTypes.number,
|
x: PropTypes.number,
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import SimpleBar from 'simplebar-react'
|
|||||||
import { useThemeContext } from '../context/ThemeContext'
|
import { useThemeContext } from '../context/ThemeContext'
|
||||||
import { filterSidebarItemsByListPermission } from '../../../database/Sidebars'
|
import { filterSidebarItemsByListPermission } from '../../../database/Sidebars'
|
||||||
import Tooltip from './Tooltip'
|
import Tooltip from './Tooltip'
|
||||||
|
import KeyboardShortcut from './KeyboardShortcut'
|
||||||
const { Sider } = Layout
|
const { Sider } = Layout
|
||||||
|
|
||||||
const CollapsedItemIcon = ({ className, icon, title }) => {
|
const CollapsedItemIcon = ({ className, icon, title }) => {
|
||||||
@ -158,13 +159,21 @@ const DashboardSidebar = ({
|
|||||||
<Flex vertical style={{ width: '100%' }}>
|
<Flex vertical style={{ width: '100%' }}>
|
||||||
<Divider style={{ margin: 0 }} />
|
<Divider style={{ margin: 0 }} />
|
||||||
<Flex style={{ padding: '4px', width: '100%' }}>
|
<Flex style={{ padding: '4px', width: '100%' }}>
|
||||||
<Button
|
<KeyboardShortcut
|
||||||
size={isElectron ? 'middle' : 'large'}
|
shortcut='alt+shift+s'
|
||||||
type='text'
|
hint='ALT ⇧ S'
|
||||||
icon={collapsed ? <ExpandSidebarIcon /> : <CollapseSidebarIcon />}
|
onTrigger={() => handleCollapse(!collapsed)}
|
||||||
style={{ flexGrow: 1, width: '100%' }}
|
>
|
||||||
onClick={() => handleCollapse(!collapsed)}
|
<Button
|
||||||
/>
|
size={isElectron ? 'middle' : 'large'}
|
||||||
|
type='text'
|
||||||
|
icon={
|
||||||
|
collapsed ? <ExpandSidebarIcon /> : <CollapseSidebarIcon />
|
||||||
|
}
|
||||||
|
style={{ flexGrow: 1, width: '100%' }}
|
||||||
|
onClick={() => handleCollapse(!collapsed)}
|
||||||
|
/>
|
||||||
|
</KeyboardShortcut>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
29
src/components/Dashboard/common/KeyboardKey.jsx
Normal file
29
src/components/Dashboard/common/KeyboardKey.jsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
const SIZE_CLASS = {
|
||||||
|
sm: '',
|
||||||
|
md: 'keyboard-key-md',
|
||||||
|
lg: 'keyboard-key-lg',
|
||||||
|
xl: 'keyboard-key-xl'
|
||||||
|
}
|
||||||
|
|
||||||
|
const KeyboardKey = ({ children, size = 'sm', className }) => {
|
||||||
|
const sizeClass = SIZE_CLASS[size] ?? ''
|
||||||
|
return (
|
||||||
|
<kbd
|
||||||
|
className={['keyboard-key', sizeClass, className]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</kbd>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyboardKey.propTypes = {
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
size: PropTypes.oneOf(['sm', 'md', 'lg', 'xl']),
|
||||||
|
className: PropTypes.string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default KeyboardKey
|
||||||
@ -1,8 +1,9 @@
|
|||||||
import { useEffect, useRef, cloneElement, useMemo } from 'react'
|
import { useEffect, useRef, cloneElement, useMemo } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { Popover, Typography } from 'antd'
|
|
||||||
import loglevel from 'loglevel'
|
import loglevel from 'loglevel'
|
||||||
import config from '../../../config'
|
import config from '../../../config'
|
||||||
|
import Tooltip from './Tooltip'
|
||||||
|
import KeyboardKey from './KeyboardKey'
|
||||||
const logger = loglevel.getLogger('ApiServerContext')
|
const logger = loglevel.getLogger('ApiServerContext')
|
||||||
logger.setLevel(config.logLevel)
|
logger.setLevel(config.logLevel)
|
||||||
|
|
||||||
@ -42,8 +43,6 @@ function getPressedKey(event) {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const { Text } = Typography
|
|
||||||
|
|
||||||
const KeyboardShortcut = ({
|
const KeyboardShortcut = ({
|
||||||
shortcut,
|
shortcut,
|
||||||
children,
|
children,
|
||||||
@ -98,17 +97,15 @@ const KeyboardShortcut = ({
|
|||||||
const element = cloneElement(children, { ref: childRef })
|
const element = cloneElement(children, { ref: childRef })
|
||||||
|
|
||||||
if (hint) {
|
if (hint) {
|
||||||
|
var osSpecificHint = hint.replaceAll('ALT', '⌥')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Tooltip
|
||||||
content={
|
hideBorder
|
||||||
<Text keyboard className='keyboard-shortcut-tooltip'>
|
title={<KeyboardKey size='md'>{osSpecificHint}</KeyboardKey>}
|
||||||
{hint}
|
|
||||||
</Text>
|
|
||||||
}
|
|
||||||
arrow={false}
|
|
||||||
>
|
>
|
||||||
{element}
|
{element}
|
||||||
</Popover>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return element
|
return element
|
||||||
|
|||||||
@ -1,10 +1,4 @@
|
|||||||
import {
|
import { cloneElement, isValidElement, useEffect, useId, useRef } from 'react'
|
||||||
cloneElement,
|
|
||||||
isValidElement,
|
|
||||||
useEffect,
|
|
||||||
useId,
|
|
||||||
useRef
|
|
||||||
} from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { useTooltipContext } from '../context/TooltipContext'
|
import { useTooltipContext } from '../context/TooltipContext'
|
||||||
|
|
||||||
@ -13,7 +7,13 @@ const mergeHandler = (original, next) => (event) => {
|
|||||||
original?.(event)
|
original?.(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Tooltip = ({ children, title, content, listenParents = 0 }) => {
|
const Tooltip = ({
|
||||||
|
children,
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
listenParents = 0,
|
||||||
|
hideBorder = false
|
||||||
|
}) => {
|
||||||
const { showTooltip, hideTooltip } = useTooltipContext()
|
const { showTooltip, hideTooltip } = useTooltipContext()
|
||||||
const id = useId()
|
const id = useId()
|
||||||
const tooltipContent = title ?? content
|
const tooltipContent = title ?? content
|
||||||
@ -21,7 +21,9 @@ const Tooltip = ({ children, title, content, listenParents = 0 }) => {
|
|||||||
const hoveredTargetsRef = useRef(new Set())
|
const hoveredTargetsRef = useRef(new Set())
|
||||||
const hoveringRef = useRef(false)
|
const hoveringRef = useRef(false)
|
||||||
const tooltipContentRef = useRef(tooltipContent)
|
const tooltipContentRef = useRef(tooltipContent)
|
||||||
|
const hideBorderRef = useRef(hideBorder)
|
||||||
tooltipContentRef.current = tooltipContent
|
tooltipContentRef.current = tooltipContent
|
||||||
|
hideBorderRef.current = hideBorder
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => hideTooltip(id)
|
return () => hideTooltip(id)
|
||||||
@ -29,14 +31,20 @@ const Tooltip = ({ children, title, content, listenParents = 0 }) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hoveringRef.current) {
|
if (hoveringRef.current) {
|
||||||
showTooltip(id, tooltipContent)
|
showTooltip(id, tooltipContent, undefined, undefined, hideBorder)
|
||||||
}
|
}
|
||||||
}, [id, showTooltip, tooltipContent])
|
}, [id, showTooltip, tooltipContent, hideBorder])
|
||||||
|
|
||||||
const onMouseEnter = (event) => {
|
const onMouseEnter = (event) => {
|
||||||
hoveredTargetsRef.current.add(event.currentTarget)
|
hoveredTargetsRef.current.add(event.currentTarget)
|
||||||
hoveringRef.current = true
|
hoveringRef.current = true
|
||||||
showTooltip(id, tooltipContentRef.current, event.clientX, event.clientY)
|
showTooltip(
|
||||||
|
id,
|
||||||
|
tooltipContentRef.current,
|
||||||
|
event.clientX,
|
||||||
|
event.clientY,
|
||||||
|
hideBorderRef.current
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onMouseLeave = (event) => {
|
const onMouseLeave = (event) => {
|
||||||
@ -82,11 +90,7 @@ const Tooltip = ({ children, title, content, listenParents = 0 }) => {
|
|||||||
}, [listenParents])
|
}, [listenParents])
|
||||||
|
|
||||||
const wrapWithSpan = (node) => (
|
const wrapWithSpan = (node) => (
|
||||||
<span
|
<span ref={spanRef} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
|
||||||
ref={spanRef}
|
|
||||||
onMouseEnter={onMouseEnter}
|
|
||||||
onMouseLeave={onMouseLeave}
|
|
||||||
>
|
|
||||||
{node}
|
{node}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
@ -115,7 +119,8 @@ 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
|
listenParents: PropTypes.number,
|
||||||
|
hideBorder: PropTypes.bool
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Tooltip
|
export default Tooltip
|
||||||
|
|||||||
@ -17,6 +17,7 @@ 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 [visible, setVisible] = useState(false)
|
||||||
|
const [hideBorder, setHideBorder] = useState(false)
|
||||||
const stackRef = useRef([])
|
const stackRef = useRef([])
|
||||||
const hideTimerRef = useRef(null)
|
const hideTimerRef = useRef(null)
|
||||||
const fadeTimerRef = useRef(null)
|
const fadeTimerRef = useRef(null)
|
||||||
@ -46,7 +47,7 @@ export const TooltipProvider = ({ children }) => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const showTooltip = useCallback(
|
const showTooltip = useCallback(
|
||||||
(id, nextContent, x, y) => {
|
(id, nextContent, x, y, nextHideBorder = false) => {
|
||||||
if (nextContent == null || nextContent === false || nextContent === '') {
|
if (nextContent == null || nextContent === false || nextContent === '') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -57,11 +58,13 @@ export const TooltipProvider = ({ children }) => {
|
|||||||
|
|
||||||
clearTimers()
|
clearTimers()
|
||||||
|
|
||||||
|
const hideBorderValue = Boolean(nextHideBorder)
|
||||||
stackRef.current = [
|
stackRef.current = [
|
||||||
...stackRef.current.filter((entry) => entry.id !== id),
|
...stackRef.current.filter((entry) => entry.id !== id),
|
||||||
{ id, content: nextContent }
|
{ id, content: nextContent, hideBorder: hideBorderValue }
|
||||||
]
|
]
|
||||||
setContent(nextContent)
|
setContent(nextContent)
|
||||||
|
setHideBorder(hideBorderValue)
|
||||||
|
|
||||||
if (!visibleRef.current) {
|
if (!visibleRef.current) {
|
||||||
window.requestAnimationFrame(() => setVisible(true))
|
window.requestAnimationFrame(() => setVisible(true))
|
||||||
@ -81,6 +84,7 @@ export const TooltipProvider = ({ children }) => {
|
|||||||
|
|
||||||
if (last) {
|
if (last) {
|
||||||
setContent(last.content)
|
setContent(last.content)
|
||||||
|
setHideBorder(Boolean(last.hideBorder))
|
||||||
setVisible(true)
|
setVisible(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -100,7 +104,12 @@ export const TooltipProvider = ({ children }) => {
|
|||||||
return (
|
return (
|
||||||
<TooltipContext.Provider value={{ showTooltip, hideTooltip }}>
|
<TooltipContext.Provider value={{ showTooltip, hideTooltip }}>
|
||||||
{children}
|
{children}
|
||||||
<CursorTooltip content={content} visible={visible} pointRef={pointRef} />
|
<CursorTooltip
|
||||||
|
content={content}
|
||||||
|
visible={visible}
|
||||||
|
pointRef={pointRef}
|
||||||
|
hideBorder={hideBorder}
|
||||||
|
/>
|
||||||
</TooltipContext.Provider>
|
</TooltipContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user