From e7f3e2a6d4280feafd0c63f4bc8b154729aa37bd Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Mon, 21 Sep 2026 00:22:51 +0100 Subject: [PATCH] Enhance Keyboard Navigation in Dashboard Contexts - 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. --- .../Dashboard/context/ActionsContext.jsx | 17 +++--- .../context/NavigationTabsContext.jsx | 53 +++++++++++++++++++ 2 files changed, 64 insertions(+), 6 deletions(-) diff --git a/src/components/Dashboard/context/ActionsContext.jsx b/src/components/Dashboard/context/ActionsContext.jsx index 9a559b6d..d04a470d 100644 --- a/src/components/Dashboard/context/ActionsContext.jsx +++ b/src/components/Dashboard/context/ActionsContext.jsx @@ -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 ( diff --git a/src/components/Dashboard/context/NavigationTabsContext.jsx b/src/components/Dashboard/context/NavigationTabsContext.jsx index 2944dc14..98a4d352 100644 --- a/src/components/Dashboard/context/NavigationTabsContext.jsx +++ b/src/components/Dashboard/context/NavigationTabsContext.jsx @@ -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]