mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2024-11-26 17:16:11 -05:00
Merge pull request #3236 from benjiwheeler/join-flow-highlighting
handle username validation errors states better
This commit is contained in:
commit
46de5a23e1
5 changed files with 81 additions and 25 deletions
|
@ -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}
|
||||||
|
|
28
src/components/formik-forms/formik-input.scss
Normal file
28
src/components/formik-forms/formik-input.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 => {
|
||||||
if (remoteResult.valid) return null;
|
// there may be multiple validation errors. Prioritize vulgarity, then
|
||||||
|
// 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});
|
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 null;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return this.props.intl.formatMessage({id: localResult.errMsgId});
|
validatePasswordIfPresent (password, username) {
|
||||||
}
|
|
||||||
validatePasswordIfPresent (password) {
|
|
||||||
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">
|
||||||
|
|
|
@ -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};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
Loading…
Reference in a new issue