Enhance Keyboard Navigation in Dashboard Contexts
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good

- Introduced key handling improvements in ActionsContext to manage control key states more effectively.
- Added keyboard navigation functionality in NavigationTabsContext, allowing users to cycle through tabs using Ctrl+Tab and select tabs with number keys.
- Implemented cleanup for key event listeners to ensure proper state management and prevent memory leaks.
This commit is contained in:
Tom Butcher 2026-09-21 00:22:51 +01:00
parent 4f6f7cb881
commit e7f3e2a6d4
2 changed files with 64 additions and 6 deletions

View File

@ -62,23 +62,27 @@ const ActionsProvider = ({ children }) => {
const [ctrlDown, setCtrlDown] = useState(false)
useEffect(() => {
const isHoldKey = (key) => key === 'Meta' || key === 'Control'
const handleKeyDown = (event) => {
const key = event.key
if (key === 'Meta') {
if (isHoldKey(event.key)) {
setCtrlDown(true)
}
}
const handleKeyUp = (event) => {
const key = event.key
if (key === 'Meta') {
if (isHoldKey(event.key)) {
setCtrlDown(false)
}
}
const clearHold = () => setCtrlDown(false)
window.addEventListener('keydown', handleKeyDown)
window.addEventListener('keyup', handleKeyUp)
window.addEventListener('blur', clearHold)
document.addEventListener('visibilitychange', clearHold)
return () => {
window.removeEventListener('keydown', handleKeyDown)
window.removeEventListener('keyup', handleKeyUp)
window.removeEventListener('blur', clearHold)
document.removeEventListener('visibilitychange', clearHold)
}
}, [])
@ -242,9 +246,10 @@ const ActionsProvider = ({ children }) => {
setCurrentObjectType,
callAction,
clearAction,
setOnModalOk
setOnModalOk,
ctrlDown
}),
[callAction, clearAction, currentObject, currentObjectType]
[callAction, clearAction, currentObject, currentObjectType, ctrlDown]
)
return (

View File

@ -971,6 +971,59 @@ export const NavigationTabsProvider = ({ children }) => {
})
}, [isElectron, onTabMovedAway, removeTab])
useEffect(() => {
if (!isElectron) return undefined
const digitIndexFromEvent = (event) => {
const code = event.code || ''
if (code.startsWith('Digit')) {
const digit = Number(code.slice(5))
if (digit >= 1 && digit <= 9) return digit - 1
if (digit === 0) return 9
}
const key = event.key
if (key >= '1' && key <= '9') return Number(key) - 1
if (key === '0') return 9
return null
}
const selectTabAtIndex = (index) => {
const nextTab = tabsRef.current[index]
if (!nextTab) return
selectTab(nextTab.id)
}
const cycleTab = (delta) => {
const current = tabsRef.current
if (current.length < 2) return
const activeIndex = current.findIndex(
(tab) => tab.id === activeTabIdRef.current
)
const fromIndex = activeIndex === -1 ? 0 : activeIndex
const nextIndex = (fromIndex + delta + current.length) % current.length
selectTab(current[nextIndex].id)
}
const handleKeyDown = (event) => {
if (event.ctrlKey && event.key === 'Tab') {
event.preventDefault()
cycleTab(event.shiftKey ? -1 : 1)
return
}
if (!(event.metaKey || event.ctrlKey) || event.repeat) return
const index = digitIndexFromEvent(event)
if (index == null) return
event.preventDefault()
selectTabAtIndex(index)
}
window.addEventListener('keydown', handleKeyDown, true)
return () => {
window.removeEventListener('keydown', handleKeyDown, true)
}
}, [isElectron, selectTab])
const activeTab = useMemo(
() => tabs.find((tab) => tab.id === activeTabId) || null,
[activeTabId, tabs]