forked from HHS/Head-Start-TTADP
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
The idle modal shows after 25 minutes of inactivity. After 30 minutes of inactivity the user is logged out of the TTA smart hub. If the user becomes active while the modal is displayed it is hidden and the 25 and 30 minute count is reset. Currently when the modal is shown the message says how long the user has until logout when the modal was first shown. I see two ways to improve the user experience: 1. We have a running countdown that updates until the user is logged out 2. We display the actual time at which the user is logged out (e.g. 10:55 am)
- Loading branch information
1 parent
1ce53c2
commit 6c9f26e
Showing
13 changed files
with
314 additions
and
337 deletions.
There are no files selected for viewing
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
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,3 @@ | ||
BACKEND_PROXY=http://localhost:8080 | ||
REACT_APP_INACTIVE_MODAL_TIMEOUT=1500000 | ||
REACT_APP_SESSION_TIMEOUT=1800000 |
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
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
/* | ||
Displays a modal after $modalTimeout milliseconds if the user is inactive. If | ||
the user becomes active when the modal is shown it will disappear. After $logoutTimeout | ||
milliseconds the logout prop is called. | ||
*/ | ||
|
||
import React, { useState, useEffect } from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import { useIdleTimer } from 'react-idle-timer'; | ||
import { | ||
Button, Modal, connectModal, useModal, Alert, | ||
} from '@trussworks/react-uswds'; | ||
|
||
// list of events to determine activity | ||
// https://github.com/SupremeTechnopriest/react-idle-timer#default-events | ||
const EVENTS = [ | ||
'keydown', | ||
'wheel', | ||
'DOMMouseSc', | ||
'mousewheel', | ||
'mousedown', | ||
'touchstart', | ||
'touchmove', | ||
'MSPointerDown', | ||
'MSPointerMove', | ||
'visibilitychange', | ||
]; | ||
|
||
function IdleModal({ modalTimeout, logoutTimeout, logoutUser }) { | ||
const [inactiveTimeout, updateInactiveTimeout] = useState(); | ||
const { isOpen, openModal, closeModal } = useModal(); | ||
const modalVisibleTime = logoutTimeout - modalTimeout; | ||
const timeoutMinutes = Math.floor(modalVisibleTime / 1000 / 60); | ||
|
||
let timeToLogoutMsg = ''; | ||
if (timeoutMinutes < 1) { | ||
timeToLogoutMsg = 'less than a minute'; | ||
} else if (timeoutMinutes === 1) { | ||
timeToLogoutMsg = 'a minute'; | ||
} else { | ||
timeToLogoutMsg = `${timeoutMinutes} minutes`; | ||
} | ||
|
||
const Connected = connectModal(() => ( | ||
<Modal | ||
title={<h3>Are you still there?</h3>} | ||
actions={( | ||
<Button type="button"> | ||
Stay logged in | ||
</Button> | ||
)} | ||
> | ||
<Alert role="alert" type="warning"> | ||
You will automatically be logged out due to inactivity in | ||
{' '} | ||
{ timeToLogoutMsg } | ||
{' '} | ||
unless you become active again. | ||
</Alert> | ||
</Modal> | ||
)); | ||
|
||
// Make sure we clean up any timeout functions when this component | ||
// is unmounted | ||
useEffect(() => function cleanup() { | ||
if (inactiveTimeout) { | ||
clearTimeout(inactiveTimeout); | ||
} | ||
}); | ||
|
||
const onIdle = () => { | ||
const timer = setTimeout(() => { | ||
closeModal(); | ||
logoutUser(true); | ||
}, modalVisibleTime); | ||
openModal(); | ||
updateInactiveTimeout(timer); | ||
}; | ||
|
||
const onActive = () => { | ||
closeModal(); | ||
clearTimeout(inactiveTimeout); | ||
}; | ||
|
||
useIdleTimer({ | ||
timeout: modalTimeout, | ||
onIdle, | ||
onActive, | ||
events: EVENTS, | ||
debounce: 500, | ||
}); | ||
|
||
return ( | ||
<Connected isOpen={isOpen} /> | ||
); | ||
} | ||
|
||
IdleModal.propTypes = { | ||
modalTimeout: PropTypes.number.isRequired, | ||
logoutTimeout: PropTypes.number.isRequired, | ||
logoutUser: PropTypes.func.isRequired, | ||
}; | ||
|
||
export default IdleModal; |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import '@testing-library/jest-dom'; | ||
import { act } from 'react-dom/test-utils'; | ||
import React from 'react'; | ||
import { render, screen, fireEvent } from '@testing-library/react'; | ||
|
||
import IdleModal from '../IdleModal'; | ||
|
||
describe('IdleModal', () => { | ||
beforeAll(() => { | ||
jest.useFakeTimers(); | ||
}); | ||
|
||
const renderIdleModal = ( | ||
logoutTimeout = 20, | ||
modalTimeout = 10, | ||
logoutUser = () => {}, | ||
) => { | ||
render( | ||
<> | ||
<div data-testid="test" /> | ||
<IdleModal | ||
logoutTimeout={logoutTimeout} | ||
modalTimeout={modalTimeout} | ||
logoutUser={logoutUser} | ||
/> | ||
</>, | ||
); | ||
}; | ||
|
||
it('modal is shown after modalTimeout', () => { | ||
renderIdleModal(); | ||
act(() => { | ||
jest.advanceTimersByTime(11); | ||
}); | ||
expect(screen.getByTestId('modal')).toBeVisible(); | ||
}); | ||
|
||
it('logout is called after logoutTimeout milliseconds of inactivity', () => { | ||
const logout = jest.fn(); | ||
renderIdleModal(20, 10, logout); | ||
act(() => { | ||
jest.advanceTimersByTime(21); | ||
}); | ||
expect(logout).toHaveBeenCalled(); | ||
}); | ||
|
||
it('modal is not shown after modalTimeout if there is activity', () => { | ||
const logout = jest.fn(); | ||
renderIdleModal(20, 10, logout); | ||
act(() => { | ||
jest.advanceTimersByTime(7); | ||
const testDiv = screen.getByTestId('test'); | ||
fireEvent.keyDown(testDiv, { key: 'Enter', code: 'Enter' }); | ||
jest.advanceTimersByTime(7); | ||
}); | ||
expect(logout).not.toHaveBeenCalled(); | ||
expect(screen.queryByTestId('modal')).toBeNull(); | ||
}); | ||
|
||
it('a shown modal is removed after action is taken', () => { | ||
const logout = jest.fn(); | ||
renderIdleModal(20, 10, logout); | ||
act(() => { | ||
jest.advanceTimersByTime(12); | ||
expect(screen.getByTestId('modal')).toBeVisible(); | ||
const testDiv = screen.getByTestId('test'); | ||
fireEvent.keyDown(testDiv, { key: 'Enter', code: 'Enter' }); | ||
}); | ||
expect(logout).not.toHaveBeenCalled(); | ||
expect(screen.queryByTestId('modal')).toBeNull(); | ||
}); | ||
|
||
describe('modal message', () => { | ||
it('shows less than a minute if logoutTimeout - modalTimeout is less than a minute', () => { | ||
renderIdleModal(20, 10); | ||
act(() => { | ||
jest.advanceTimersByTime(11); | ||
expect(screen.getByRole('alert').textContent).toContain('in less than a minute'); | ||
}); | ||
}); | ||
|
||
it('shows a minute if logoutTimeout - modalTimeout is a minute', () => { | ||
renderIdleModal(1000 * 60 + 10, 10); | ||
act(() => { | ||
jest.advanceTimersByTime(11); | ||
expect(screen.getByRole('alert').textContent).toContain('in a minute'); | ||
}); | ||
}); | ||
|
||
it('shows logoutTimeout - modalTimeout minutes when greater than one minute', () => { | ||
renderIdleModal((1000 * 60 * 5) + 10, 10); | ||
act(() => { | ||
jest.advanceTimersByTime(11); | ||
expect(screen.getByRole('alert').textContent).toContain('in 5 minutes'); | ||
}); | ||
}); | ||
}); | ||
}); |
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
Oops, something went wrong.