Merge pull request #3236 from benjiwheeler/join-flow-highlighting

handle username validation errors states better
This commit is contained in:
Benjamin Wheeler 2019-08-13 17:52:00 -04:00 committed by GitHub
commit 46de5a23e1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 81 additions and 25 deletions

View file

@ -5,9 +5,8 @@ import {Field} from 'formik';
const ValidationMessage = require('../forms/validation-message.jsx'); const ValidationMessage = require('../forms/validation-message.jsx');
require('./input.scss');
require('../forms/input.scss');
require('../forms/row.scss'); require('../forms/row.scss');
require('./formik-input.scss');
const FormikInput = ({ const FormikInput = ({
className, className,
@ -27,6 +26,7 @@ const FormikInput = ({
<Field <Field
className={classNames( className={classNames(
'input', 'input',
{fail: error},
className className
)} )}
{...props} {...props}

View file

@ -0,0 +1,28 @@
@import "../../colors";
.input {
height: 2.75rem;
border-radius: .5rem;
background-color: $ui-white;
margin-bottom: .5rem;
transition: all .5s ease;
border: 1px solid $active-gray;
padding: 0 1rem;
color: $type-gray;
font-size: .875rem;
&:focus {
box-shadow: 0 0 0 .25rem $ui-blue-25percent;
outline: none;
border: 1px solid $ui-blue;
}
&.fail {
border: 1px solid $ui-orange;
&:focus {
box-shadow: 0 0 0 .25rem $ui-orange-25percent;
outline: none;
}
}
}

View file

@ -36,20 +36,26 @@ class UsernameStep extends React.Component {
// we allow username to be empty on blur, since you might not have typed anything yet // we allow username to be empty on blur, since you might not have typed anything yet
validateUsernameIfPresent (username) { validateUsernameIfPresent (username) {
if (!username) return null; // skip validation if username is blank; null indicates valid if (!username) return null; // skip validation if username is blank; null indicates valid
// if username is not blank, run both local and remote validations
const localResult = validate.validateUsernameLocally(username); const localResult = validate.validateUsernameLocally(username);
if (localResult.valid) { return validate.validateUsernameRemotely(username).then(
return validate.validateUsernameRemotely(username).then( remoteResult => {
remoteResult => { // there may be multiple validation errors. Prioritize vulgarity, then
if (remoteResult.valid) return null; // length, then having invalid chars, then all other remote reports
if (remoteResult.valid === false && remoteResult.errMsgId === 'registration.validationUsernameVulgar') {
return this.props.intl.formatMessage({id: remoteResult.errMsgId});
} else if (localResult.valid === false) {
return this.props.intl.formatMessage({id: localResult.errMsgId});
} else if (remoteResult.valid === false) {
return this.props.intl.formatMessage({id: remoteResult.errMsgId}); return this.props.intl.formatMessage({id: remoteResult.errMsgId});
} }
); return null;
} }
return this.props.intl.formatMessage({id: localResult.errMsgId}); );
} }
validatePasswordIfPresent (password) { validatePasswordIfPresent (password, username) {
if (!password) return null; // skip validation if password is blank; null indicates valid if (!password) return null; // skip validation if password is blank; null indicates valid
const localResult = validate.validatePassword(password); const localResult = validate.validatePassword(password, username);
if (localResult.valid) return null; if (localResult.valid) return null;
return this.props.intl.formatMessage({id: localResult.errMsgId}); return this.props.intl.formatMessage({id: localResult.errMsgId});
} }
@ -69,13 +75,10 @@ class UsernameStep extends React.Component {
if (!usernameResult.valid) { if (!usernameResult.valid) {
errors.username = this.props.intl.formatMessage({id: usernameResult.errMsgId}); errors.username = this.props.intl.formatMessage({id: usernameResult.errMsgId});
} }
const passwordResult = validate.validatePassword(values.password); const passwordResult = validate.validatePassword(values.password, values.username);
if (!passwordResult.valid) { if (!passwordResult.valid) {
errors.password = this.props.intl.formatMessage({id: passwordResult.errMsgId}); errors.password = this.props.intl.formatMessage({id: passwordResult.errMsgId});
} }
if (values.password === values.username) {
errors.password = this.props.intl.formatMessage({id: 'registration.validationPasswordNotUsername'});
}
const passwordConfirmResult = validate.validatePasswordConfirm(values.password, values.passwordConfirm); const passwordConfirmResult = validate.validatePasswordConfirm(values.password, values.passwordConfirm);
if (!passwordConfirmResult.valid) { if (!passwordConfirmResult.valid) {
errors.passwordConfirm = this.props.intl.formatMessage({id: passwordConfirmResult.errMsgId}); errors.passwordConfirm = this.props.intl.formatMessage({id: passwordConfirmResult.errMsgId});
@ -105,6 +108,8 @@ class UsernameStep extends React.Component {
errors, errors,
handleSubmit, handleSubmit,
isSubmitting, isSubmitting,
setFieldError,
setFieldValue,
validateField, validateField,
values values
} = props; } = props;
@ -123,15 +128,20 @@ class UsernameStep extends React.Component {
</div> </div>
<FormikInput <FormikInput
className={classNames( className={classNames(
'join-flow-input', 'join-flow-input'
{fail: errors.username}
)} )}
error={errors.username} error={errors.username}
id="username" id="username"
name="username" name="username"
validate={this.validateUsernameIfPresent} validate={this.validateUsernameIfPresent}
validationClassName="validation-full-width-input" validationClassName="validation-full-width-input"
onBlur={() => validateField('username')} // eslint-disable-line react/jsx-no-bind /* eslint-disable react/jsx-no-bind */
onBlur={() => validateField('username')}
onChange={e => {
setFieldValue('username', e.target.value);
setFieldError('username', null);
}}
/* eslint-enable react/jsx-no-bind */
/> />
<div className="join-flow-password-section"> <div className="join-flow-password-section">
<div className="join-flow-input-title"> <div className="join-flow-input-title">
@ -139,23 +149,25 @@ class UsernameStep extends React.Component {
</div> </div>
<FormikInput <FormikInput
className={classNames( className={classNames(
'join-flow-input', 'join-flow-input'
{fail: errors.password}
)} )}
error={errors.password} error={errors.password}
id="password" id="password"
name="password" name="password"
type={this.state.showPassword ? 'text' : 'password'} type={this.state.showPassword ? 'text' : 'password'}
validate={this.validatePasswordIfPresent}
validationClassName="validation-full-width-input"
/* eslint-disable react/jsx-no-bind */ /* eslint-disable react/jsx-no-bind */
validate={password => this.validatePasswordIfPresent(password, values.username)}
validationClassName="validation-full-width-input"
onBlur={() => validateField('password')} onBlur={() => validateField('password')}
onChange={e => {
setFieldValue('password', e.target.value);
setFieldError('password', null);
}}
/* eslint-enable react/jsx-no-bind */ /* eslint-enable react/jsx-no-bind */
/> />
<FormikInput <FormikInput
className={classNames( className={classNames(
'join-flow-input', 'join-flow-input'
{fail: errors.passwordConfirm}
)} )}
error={errors.passwordConfirm} error={errors.passwordConfirm}
id="passwordConfirm" id="passwordConfirm"
@ -170,6 +182,10 @@ class UsernameStep extends React.Component {
onBlur={() => onBlur={() =>
validateField('passwordConfirm') validateField('passwordConfirm')
} }
onChange={e => {
setFieldValue('passwordConfirm', e.target.value);
setFieldError('passwordConfirm', null);
}}
/* eslint-enable react/jsx-no-bind */ /* eslint-enable react/jsx-no-bind */
/> />
<div className="join-flow-input-title"> <div className="join-flow-input-title">

View file

@ -40,13 +40,21 @@ module.exports.validateUsernameRemotely = username => (
}) })
); );
module.exports.validatePassword = password => { /**
* Validate password value, optionally also considering username value
* @param {string} password password value to validate
* @param {string} username username value to compare
* @return {object} {valid: boolean, errMsgId: string}
*/
module.exports.validatePassword = (password, username) => {
if (!password) { if (!password) {
return {valid: false, errMsgId: 'general.required'}; return {valid: false, errMsgId: 'general.required'};
} else if (password.length < 6) { } else if (password.length < 6) {
return {valid: false, errMsgId: 'registration.validationPasswordLength'}; return {valid: false, errMsgId: 'registration.validationPasswordLength'};
} else if (password === 'password') { } else if (password === 'password') {
return {valid: false, errMsgId: 'registration.validationPasswordNotEquals'}; return {valid: false, errMsgId: 'registration.validationPasswordNotEquals'};
} else if (username && password === username) {
return {valid: false, errMsgId: 'registration.validationPasswordNotUsername'};
} }
return {valid: true}; return {valid: true};
}; };

View file

@ -39,6 +39,10 @@ describe('unit test lib/validate.js', () => {
expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordLength'}); expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordLength'});
response = validate.validatePassword('password'); response = validate.validatePassword('password');
expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordNotEquals'}); expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordNotEquals'});
response = validate.validatePassword('abcdefg', 'abcdefg');
expect(response).toEqual({valid: false, errMsgId: 'registration.validationPasswordNotUsername'});
response = validate.validatePassword('abcdefg', 'abcdefG');
expect(response).toEqual({valid: true});
}); });
test('validate password confirm', () => { test('validate password confirm', () => {