Created
June 30, 2020 22:07
-
-
Save cpitt/f98b774a776920393d2f0d88ee66127c to your computer and use it in GitHub Desktop.
useIdleTimer React hook
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { renderHook, act } from '@testing-library/react-hooks'; | |
import useIdleTimer from './useIdleTimer'; | |
import { fireEvent } from '@testing-library/react'; | |
jest.useFakeTimers(); | |
describe('useIdleTimer', function() { | |
it('should start timer when startIdleTimer is called', function() { | |
const { result } = renderHook(() => useIdleTimer()); | |
act(() => { | |
result.current.startIdleTimer(); | |
jest.advanceTimersByTime(1000); | |
}); | |
expect(result.current.idleTime).toBeGreaterThan(0); | |
}); | |
it('should return idleTimeoutWarning true if threshold exceeded false if not', function() { | |
const { result } = renderHook(() => useIdleTimer(5, 10)); | |
act(() => { | |
result.current.startIdleTimer(); | |
jest.advanceTimersByTime(1000); | |
}); | |
expect(result.current.idleTimeoutWarning).toBe(false); | |
act(() => { | |
jest.advanceTimersByTime(6000); | |
}); | |
expect(result.current.idleTimeoutWarning).toBe(true); | |
}); | |
it('should return idleTimeout true if threshold exceeded false if not', function() { | |
const { result } = renderHook(() => useIdleTimer(5, 10)); | |
act(() => { | |
result.current.startIdleTimer(); | |
jest.advanceTimersByTime(1000); | |
}); | |
expect(result.current.idleTimeout).toBe(false); | |
act(() => { | |
jest.advanceTimersByTime(11000); | |
}); | |
expect(result.current.idleTimeout).toBe(true); | |
}); | |
it('should reset timer if timeout thresholds not met on active event', function() { | |
const { result } = renderHook(() => useIdleTimer(5, 10)); | |
act(() => { | |
result.current.startIdleTimer(); | |
jest.advanceTimersByTime(1000); | |
fireEvent.mouseMove(window); | |
}); | |
expect(result.current.idleTime).toBe(0); | |
act(() => { | |
jest.advanceTimersByTime(10001); | |
fireEvent.mouseMove(window); | |
}); | |
expect(result.current.idleTime).toBe(10); | |
}); | |
it('should reset timer', function() { | |
const { result } = renderHook(() => useIdleTimer(5, 10)); | |
act(() => { | |
result.current.startIdleTimer(); | |
jest.advanceTimersByTime(1000); | |
}); | |
expect(result.current.idleTime).toBe(1); | |
act(() => { | |
result.current.resetIdleTimer(); | |
}); | |
expect(result.current.idleTime).toBe(0); | |
}); | |
it('should stop timer', function() { | |
const { result } = renderHook(() => useIdleTimer(5, 10)); | |
act(() => { | |
result.current.startIdleTimer(); | |
jest.advanceTimersByTime(1000); | |
}); | |
expect(result.current.idleTime).toBe(1); | |
act(() => { | |
result.current.stopIdleTimer(); | |
jest.advanceTimersByTime(1000); | |
}); | |
expect(result.current.idleTime).toBe(1); | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useEffect, useState } from 'react'; | |
const events = [ | |
'mousemove', | |
'keydown', | |
'wheel', | |
'DOMMouseScroll', | |
'mouseWheel', | |
'mousedown', | |
'touchstart', | |
'touchmove', | |
'MSPointerDown', | |
'MSPointerMove', | |
'visibilitychange', | |
]; | |
/** | |
* creates an idle timer with warning and timeout threshold | |
* @param { number } warningThreshold time in seconds before | |
* trigging warning state defaults to 1500 (25 mins) | |
* @param { number } timeoutThreshold time in seconds before | |
* triggering timeout state change defaults 1800 (30 mins) | |
*/ | |
const useIdleTimer = ( | |
warningThreshold: number = 25 * 60, | |
timeoutThreshold: number = 30 * 60, | |
) => { | |
let idleInterval: NodeJS.Timer; | |
const [isRunning, setIsRunning] = useState(false); | |
const initState = { | |
idleTime: 0, | |
idleTimeoutWarning: false, | |
idleTimeout: false, | |
}; | |
const [state, setState] = useState(initState); | |
const handleIdleInterval = () => | |
setState(prevState => ({ | |
idleTime: prevState.idleTime + 1, | |
idleTimeoutWarning: | |
prevState.idleTimeoutWarning || prevState.idleTime > warningThreshold, | |
idleTimeout: | |
prevState.idleTimeout || prevState.idleTime > timeoutThreshold, | |
})); | |
const startIdleTimer = () => setIsRunning(true); | |
const stopIdleTimer = () => setIsRunning(false); | |
const resetIdleTimer = () => setState({ ...initState }); | |
const handleEvent = () => { | |
// Do not reset the timer if we've hit a warning or timeout | |
setState(prevState => ({ | |
...prevState, | |
idleTime: | |
prevState.idleTimeout || prevState.idleTimeoutWarning | |
? prevState.idleTime | |
: 0, | |
})); | |
}; | |
const tearDown = () => { | |
clearInterval(idleInterval); | |
events.forEach(event => { | |
window.removeEventListener(event, handleEvent); | |
}); | |
}; | |
const init = () => { | |
idleInterval = setInterval(handleIdleInterval, 1000); | |
events.forEach(event => { | |
window.addEventListener(event, handleEvent); | |
}); | |
}; | |
useEffect(() => { | |
if (isRunning) { | |
init(); | |
} else { | |
tearDown(); | |
} | |
return () => tearDown(); | |
}, [isRunning]); | |
return { | |
...state, | |
startIdleTimer, | |
stopIdleTimer, | |
resetIdleTimer, | |
}; | |
}; | |
export default useIdleTimer; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment