[FEATURE] Autofocus on authentication and OTP pages (#806)

* [FEATURE] Autofocus on authentication and OTP pages
This change sets the input focus on the first factor authentication and OTP pages.

The behaviour for the first factor authentication page has also been amended slightly, if an incorrect username or password is provided the password field will be cleared and set as the focus.

One thing to note is that the OTP page does not focus on any re-rendering and this is because the component doesn't handle focusing. This means that the OTP input only is auto-focused when you first visit it, if you enter an incorrect OTP there will be no focus.

Ideally we should be looking for a different library or writing a component for this ourselves in future.

Closes #511.

* Add TODO markers for potential refactor
This commit is contained in:
Amir Zarrinkafsh 2020-04-01 10:27:54 +11:00 committed by GitHub
parent 6128081e1f
commit d82b46a3ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 30 additions and 13 deletions

View File

@ -72,7 +72,7 @@ func (s *OneFactorSuite) TestShouldDenyAccessOnBadPassword() {
targetURL := fmt.Sprintf("%s/secret.html", AdminBaseURL) targetURL := fmt.Sprintf("%s/secret.html", AdminBaseURL)
s.doLoginOneFactor(ctx, s.T(), "john", "bad-password", false, targetURL) s.doLoginOneFactor(ctx, s.T(), "john", "bad-password", false, targetURL)
s.verifyIsFirstFactorPage(ctx, s.T()) s.verifyIsFirstFactorPage(ctx, s.T())
s.verifyNotificationDisplayed(ctx, s.T(), "There was a problem. Username or password might be incorrect.") s.verifyNotificationDisplayed(ctx, s.T(), "Incorrect username or password.")
} }
func TestRunOneFactor(t *testing.T) { func TestRunOneFactor(t *testing.T) {

View File

@ -7,7 +7,6 @@ import (
"time" "time"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/tebeka/selenium"
) )
type RegulationScenario struct { type RegulationScenario struct {
@ -53,27 +52,26 @@ func (s *RegulationScenario) TestShouldBanUserAfterTooManyAttempt() {
s.doVisitLoginPage(ctx, s.T(), "") s.doVisitLoginPage(ctx, s.T(), "")
s.doFillLoginPageAndClick(ctx, s.T(), "john", "bad-password", false) s.doFillLoginPageAndClick(ctx, s.T(), "john", "bad-password", false)
s.verifyNotificationDisplayed(ctx, s.T(), "There was a problem. Username or password might be incorrect.") s.verifyNotificationDisplayed(ctx, s.T(), "Incorrect username or password.")
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
s.WaitElementLocatedByID(ctx, s.T(), "password-textfield").SendKeys("bad-password")
s.WaitElementLocatedByID(ctx, s.T(), "sign-in-button").Click() s.WaitElementLocatedByID(ctx, s.T(), "sign-in-button").Click()
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
} }
// Reset password field // Enter the correct password and test the regulation lock out
s.WaitElementLocatedByID(ctx, s.T(), "password-textfield").
SendKeys(selenium.ControlKey + "a" + selenium.BackspaceKey)
// And enter the correct password
s.WaitElementLocatedByID(ctx, s.T(), "password-textfield").SendKeys("password") s.WaitElementLocatedByID(ctx, s.T(), "password-textfield").SendKeys("password")
s.WaitElementLocatedByID(ctx, s.T(), "sign-in-button").Click() s.WaitElementLocatedByID(ctx, s.T(), "sign-in-button").Click()
s.verifyNotificationDisplayed(ctx, s.T(), "There was a problem. Username or password might be incorrect.") s.verifyNotificationDisplayed(ctx, s.T(), "Incorrect username or password.")
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
s.verifyIsFirstFactorPage(ctx, s.T()) s.verifyIsFirstFactorPage(ctx, s.T())
time.Sleep(9 * time.Second) time.Sleep(9 * time.Second)
// Enter the correct password and test a successful login
s.WaitElementLocatedByID(ctx, s.T(), "password-textfield").SendKeys("password")
s.WaitElementLocatedByID(ctx, s.T(), "sign-in-button").Click() s.WaitElementLocatedByID(ctx, s.T(), "sign-in-button").Click()
s.verifyIsSecondFactorPage(ctx, s.T()) s.verifyIsSecondFactorPage(ctx, s.T())
} }

View File

@ -56,7 +56,7 @@ func (s *ResetPasswordScenario) TestShouldResetPassword() {
// Try to login with the old password // Try to login with the old password
s.doLoginOneFactor(ctx, s.T(), "john", "password", false, "") s.doLoginOneFactor(ctx, s.T(), "john", "password", false, "")
s.verifyNotificationDisplayed(ctx, s.T(), "There was a problem. Username or password might be incorrect.") s.verifyNotificationDisplayed(ctx, s.T(), "Incorrect username or password.")
// Try to login with the new password // Try to login with the new password
s.doLoginOneFactor(ctx, s.T(), "john", "abc", false, "") s.doLoginOneFactor(ctx, s.T(), "john", "abc", false, "")

View File

@ -1,4 +1,4 @@
import React, { useState } from "react"; import React, { MutableRefObject, useEffect, useRef, useState } from "react";
import classnames from "classnames"; import classnames from "classnames";
import { makeStyles, Grid, Button, FormControlLabel, Checkbox, Link } from "@material-ui/core"; import { makeStyles, Grid, Button, FormControlLabel, Checkbox, Link } from "@material-ui/core";
import { useHistory } from "react-router"; import { useHistory } from "react-router";
@ -28,6 +28,13 @@ export default function (props: Props) {
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [passwordError, setPasswordError] = useState(false); const [passwordError, setPasswordError] = useState(false);
const { createErrorNotification } = useNotifications(); const { createErrorNotification } = useNotifications();
// TODO (PR: #806, Issue: #511) potentially refactor
const usernameRef = useRef() as MutableRefObject<HTMLInputElement>;
const passwordRef = useRef() as MutableRefObject<HTMLInputElement>;
useEffect(() => {
const timeout = setTimeout(() => usernameRef.current.focus(), 10);
return () => clearTimeout(timeout);
}, [usernameRef]);
const disabled = props.disabled; const disabled = props.disabled;
@ -54,8 +61,10 @@ export default function (props: Props) {
} catch (err) { } catch (err) {
console.error(err); console.error(err);
createErrorNotification( createErrorNotification(
"There was a problem. Username or password might be incorrect."); "Incorrect username or password.");
props.onAuthenticationFailure(); props.onAuthenticationFailure();
setPassword("");
passwordRef.current.focus();
} }
} }
@ -71,6 +80,8 @@ export default function (props: Props) {
<Grid container spacing={2} className={style.root}> <Grid container spacing={2} className={style.root}>
<Grid item xs={12}> <Grid item xs={12}>
<FixedTextField <FixedTextField
// TODO (PR: #806, Issue: #511) potentially refactor
inputRef={usernameRef}
id="username-textfield" id="username-textfield"
label="Username" label="Username"
variant="outlined" variant="outlined"
@ -80,10 +91,17 @@ export default function (props: Props) {
disabled={disabled} disabled={disabled}
fullWidth fullWidth
onChange={v => setUsername(v.target.value)} onChange={v => setUsername(v.target.value)}
onFocus={() => setUsernameError(false)} /> onFocus={() => setUsernameError(false)}
onKeyPress={(ev) => {
if (ev.key === 'Enter') {
passwordRef.current.focus();
}
}} />
</Grid> </Grid>
<Grid item xs={12}> <Grid item xs={12}>
<FixedTextField <FixedTextField
// TODO (PR: #806, Issue: #511) potentially refactor
inputRef={passwordRef}
id="password-textfield" id="password-textfield"
label="Password" label="Password"
variant="outlined" variant="outlined"

View File

@ -20,6 +20,7 @@ export default function (props: Props) {
const dial = ( const dial = (
<span className={style.otpInput} id="otp-input"> <span className={style.otpInput} id="otp-input">
<OtpInput <OtpInput
shouldAutoFocus
onChange={props.onChange} onChange={props.onChange}
value={props.passcode} value={props.passcode}
numInputs={6} numInputs={6}