Add navigation gesture support in mainWindow and ElectronContext
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good

- Implemented navigation gestures in mainWindow.js to handle swipe and app-command events for back and forward navigation.
- Enhanced ElectronContext.jsx to manage navigation history through IPC events and added horizontal scroll detection for improved user experience.
- Introduced a debounce mechanism for navigation actions to prevent rapid triggering.
This commit is contained in:
Tom Butcher 2026-07-28 02:54:29 +01:00
parent f0571c9c5c
commit 36a3e81f47
2 changed files with 97 additions and 2 deletions

View File

@ -149,6 +149,33 @@ function attachKeyboardShortcuts(browserWindow) {
// Keyboard shortcuts for the main window can be added here if needed
}
function sendNavigationGesture(browserWindow, direction) {
if (!browserWindow || browserWindow.isDestroyed()) return
browserWindow.webContents.send('navigation-gesture', direction)
}
function setupNavigationGestures(browserWindow) {
if (!browserWindow) return
if (process.platform === 'darwin') {
browserWindow.on('swipe', (_event, direction) => {
if (direction === 'left') {
sendNavigationGesture(browserWindow, 'back')
} else if (direction === 'right') {
sendNavigationGesture(browserWindow, 'forward')
}
})
}
browserWindow.on('app-command', (_event, command) => {
if (command === 'browser-backward') {
sendNavigationGesture(browserWindow, 'back')
} else if (command === 'browser-forward') {
sendNavigationGesture(browserWindow, 'forward')
}
})
}
function setupWindowEvents() {
if (!win) return
win.on('maximize', () => {
@ -199,6 +226,7 @@ export function createWindow() {
setupWindowEvents()
attachKeyboardShortcuts(win)
setupNavigationGestures(win)
}
export function getWindow() {

View File

@ -1,4 +1,4 @@
import { createContext, useCallback, useEffect, useState } from 'react'
import { createContext, useCallback, useEffect, useRef, useState } from 'react'
import PropTypes from 'prop-types'
import { useNavigate } from 'react-router-dom'
@ -32,12 +32,43 @@ export function isElectron() {
const ElectronContext = createContext()
function isInHorizontalScrollContainer(element) {
let node = element
while (node && node !== document.body) {
const { overflowX } = window.getComputedStyle(node)
if (
(overflowX === 'auto' || overflowX === 'scroll') &&
node.scrollWidth > node.clientWidth
) {
return true
}
node = node.parentElement
}
return false
}
const ElectronProvider = ({ children }) => {
const [platform, setPlatform] = useState('unknown')
const [isMaximized, setIsMaximized] = useState(false)
const [isFullScreen, setIsFullScreen] = useState(false)
const [electronAvailable] = useState(isElectron())
const navigate = useNavigate()
const lastNavigationAtRef = useRef(0)
const navigateHistory = useCallback(
(direction) => {
const now = Date.now()
if (now - lastNavigationAtRef.current < 300) return
lastNavigationAtRef.current = now
if (direction === 'back') {
navigate(-1)
} else if (direction === 'forward') {
navigate(1)
}
},
[navigate]
)
// Function to open external URL via Electron
const openExternalUrl = (url) => {
@ -92,11 +123,47 @@ const ElectronProvider = ({ children }) => {
}
ipcRenderer.on('navigate', navigateHandler)
const navigationGestureHandler = (_event, direction) => {
navigateHistory(direction)
}
ipcRenderer.on('navigation-gesture', navigationGestureHandler)
return () => {
ipcRenderer.removeListener('navigate', navigateHandler)
ipcRenderer.removeListener('navigation-gesture', navigationGestureHandler)
ipcRenderer.removeListener('window-state', windowStateHandler)
}
}, [navigate])
}, [navigate, navigateHistory])
useEffect(() => {
if (!electronAvailable || platform !== 'darwin') return
let accumulatedDeltaX = 0
let resetTimer
const handleWheel = (event) => {
if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) return
if (Math.abs(event.deltaX) < Math.abs(event.deltaY)) return
if (Math.abs(event.deltaX) < 2) return
if (isInHorizontalScrollContainer(event.target)) return
accumulatedDeltaX += event.deltaX
clearTimeout(resetTimer)
resetTimer = setTimeout(() => {
if (Math.abs(accumulatedDeltaX) > 60) {
navigateHistory(accumulatedDeltaX > 0 ? 'forward' : 'back')
}
accumulatedDeltaX = 0
}, 80)
}
window.addEventListener('wheel', handleWheel, { passive: true })
return () => {
window.removeEventListener('wheel', handleWheel)
clearTimeout(resetTimer)
}
}, [electronAvailable, navigateHistory, platform])
// Window control handler
const handleWindowControl = (action) => {