Skip to content

Commit

Permalink
fix(auth): validate registration form on blur and improve password st…
Browse files Browse the repository at this point in the history
…rength
  • Loading branch information
ChristiaanScheermeijer committed Aug 4, 2021
1 parent b774a42 commit 89644bc
Show file tree
Hide file tree
Showing 12 changed files with 153 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,20 @@ exports[`<EditPasswordForm> renders and matches snapshot 1`] = `
</div>
<div
class="passwordStrength"
data-strength="0"
>
<div
class="passwordStrengthBar"
>
<div
class="passwordStrengthFill"
data-strength="0"
/>
</div>
<span>
registration.password_strength
<span
class="label"
>
registration.password_strength.invalid
</span>
</div>
<button
Expand Down
48 changes: 27 additions & 21 deletions src/components/PasswordStrength/PasswordStrength.module.scss
Original file line number Diff line number Diff line change
@@ -1,17 +1,38 @@
@use '../../styles/variables';
@use '../../styles/theme';

@mixin strength($strength, $width, $color) {
&[data-strength="#{$strength}"] {
.passwordStrengthFill {
width: $width;
background: $color;
}

.label {
color: $color;
}
}
}

.passwordStrength {
@include strength(1, 25%, orangered);
@include strength(2, 50%, orange);
@include strength(3, 75%, yellowgreen);
@include strength(4, 100%, green);

position: relative;
margin: variables.$base-spacing 0;
display: flex;
align-items: center;
height: 16px;
margin: 8px 0;
font-size: 14px;
}

.passwordStrengthBar {
position: relative;
width: 170px;
height: 6px;
margin: variables.$base-spacing 0;
margin-right: 8px;

background: #ddd;
border-radius: 5px;
Expand All @@ -20,28 +41,13 @@
.passwordStrengthFill {
position: absolute;
width: 0;
height: inherit;
height: 100%;
background: transparent;
border-radius: inherit;
transition: width 0.5s ease-in-out, background 0.25s;
}

.passwordStrengthFill[data-strength='1'] {
width: 25%;
background: orangered;
}

.passwordStrengthFill[data-strength='2'] {
width: 50%;
background: orange;
}

.passwordStrengthFill[data-strength='3'] {
width: 75%;
background: yellowgreen;
}

.passwordStrengthFill[data-strength='4'] {
width: 100%;
background: green;
.label {
font-weight: theme.$body-font-weight-bold;
font-size: 14px;
}
33 changes: 25 additions & 8 deletions src/components/PasswordStrength/PasswordStrength.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,49 @@ type Props = {
password: string;
};

const PASSWORD_REGEX = /^(?=.*[a-z])(?=.*[0-9]).{8,}$/;

const PasswordStrength: React.FC<Props> = ({ password }: Props) => {
const { t } = useTranslation('account');

const passwordStrength = (password: string) => {
let strength = 0;

if (password.match(/[a-z]+/)) {
if (!password.match(PASSWORD_REGEX)) return strength;

if (password.match(/[A-Z]+/)) {
strength += 1;
}
if (password.match(/[A-Z]+/)) {

if (password.match(/(\d.*\d)/)) {
strength += 1;
}
if (password.match(/[0-9|!@#$%^&*()_+-=]+/)) {

if (password.match(/[!,@#$%^&*?_~]/)) {
strength += 1;
}
if (password.length >= 6) {

if (password.match(/([!,@#$%^&*?_~].*[!,@#$%^&*?_~])/)) {
strength += 1;
}

return strength;
};
const strength = passwordStrength(password);
const labels = [
t('registration.password_strength.invalid'),
t('registration.password_strength.weak'),
t('registration.password_strength.fair'),
t('registration.password_strength.strong'),
t('registration.password_strength.very_strong'),
];

return (
<div className={styles.passwordStrength}>
<div className={styles.passwordStrength} data-strength={strength}>
<div className={styles.passwordStrengthBar}>
<div className={styles.passwordStrengthFill} data-strength={passwordStrength(password)}></div>
</div>
<span>{t('registration.password_strength')}</span>
<div className={styles.passwordStrengthFill} />
</div>{' '}
<span className={styles.label}>{labels[strength]}</span>
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,20 @@ exports[`<PasswordStrength> renders and matches snapshot 1`] = `
<div>
<div
class="passwordStrength"
data-strength="2"
>
<div
class="passwordStrengthBar"
>
<div
class="passwordStrengthFill"
data-strength="4"
/>
</div>
<span>
registration.password_strength
<span
class="label"
>
registration.password_strength.fair
</span>
</div>
</div>
Expand Down
12 changes: 10 additions & 2 deletions src/components/RegistrationForm/RegistrationForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import styles from './RegistrationForm.module.scss';
type Props = {
onSubmit: React.FormEventHandler<HTMLFormElement>;
onChange: React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement>;
onBlur: React.FocusEventHandler<HTMLInputElement | HTMLTextAreaElement>;
onConsentChange: React.ChangeEventHandler<HTMLInputElement>;
errors: FormErrors<RegistrationFormData>;
values: RegistrationFormData;
Expand All @@ -36,6 +37,7 @@ type Props = {
const RegistrationForm: React.FC<Props> = ({
onSubmit,
onChange,
onBlur,
values,
errors,
submitting,
Expand Down Expand Up @@ -78,6 +80,7 @@ const RegistrationForm: React.FC<Props> = ({
<TextField
value={values.email}
onChange={onChange}
onBlur={onBlur}
label={t('registration.email')}
placeholder={t('registration.email')}
error={!!errors.email || !!errors.form}
Expand All @@ -89,10 +92,16 @@ const RegistrationForm: React.FC<Props> = ({
<TextField
value={values.password}
onChange={onChange}
onBlur={onBlur}
label={t('registration.password')}
placeholder={t('registration.password')}
error={!!errors.password || !!errors.form}
helperText={errors.password}
helperText={(
<React.Fragment>
<PasswordStrength password={values.password} />
{t('registration.password_helper_text')}
</React.Fragment>
)}
name="password"
type={viewPassword ? 'text' : 'password'}
rightControl={
Expand All @@ -105,7 +114,6 @@ const RegistrationForm: React.FC<Props> = ({
}
required
/>
<PasswordStrength password={values.password} />
{publisherConsents?.map((consent, index) => (
<Checkbox
key={index}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,21 +80,29 @@ exports[`<RegistrationForm> renders and matches snapshot 1`] = `
</div>
</div>
</div>
</div>
<div
class="passwordStrength"
>
<div
class="passwordStrengthBar"
class="helperText"
>
<div
class="passwordStrengthFill"
class="passwordStrength"
data-strength="0"
/>
>
<div
class="passwordStrengthBar"
>
<div
class="passwordStrengthFill"
/>
</div>
<span
class="label"
>
registration.password_strength.invalid
</span>
</div>
registration.password_helper_text
</div>
<span>
registration.password_strength
</span>
</div>
<button
aria-disabled="false"
Expand Down
3 changes: 2 additions & 1 deletion src/components/TextField/TextField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ type Props = {
value: string;
type?: 'text' | 'email' | 'password' | 'search' | 'number' | 'date';
onChange?: React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement>;
onFocus?: React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement>;
onFocus?: React.FocusEventHandler<HTMLInputElement | HTMLTextAreaElement>;
onBlur?: React.FocusEventHandler<HTMLInputElement | HTMLTextAreaElement>;
helperText?: React.ReactNode;
leftControl?: React.ReactNode;
rightControl?: React.ReactNode;
Expand Down
5 changes: 3 additions & 2 deletions src/containers/AccountModal/forms/Registration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,17 @@ const Registration = () => {

const validationSchema: SchemaOf<RegistrationFormData> = object().shape({
email: string().email(t('registration.field_is_not_valid_email')).required(t('registration.field_required')),
password: string().required(t('registration.field_required')),
password: string().matches(/^(?=.*[a-z])(?=.*[0-9]).{8,}$/, t('registration.invalid_password')).required(t('registration.field_required')),
});

const initialRegistrationValues: RegistrationFormData = { email: '', password: '' };
const { handleSubmit, handleChange, values, errors, submitting } = useForm(initialRegistrationValues, registrationSubmitHandler, validationSchema);
const { handleSubmit, handleChange, handleBlur, values, errors, submitting } = useForm(initialRegistrationValues, registrationSubmitHandler, validationSchema, true);

return (
<RegistrationForm
onSubmit={handleSubmit}
onChange={handleChange}
onBlur={handleBlur}
values={values}
errors={errors}
consentErrors={consentErrors}
Expand Down
41 changes: 38 additions & 3 deletions src/hooks/useForm.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { useState } from 'react';
import type { FormErrors, GenericFormValues, UseFormChangeHandler, UseFormSubmitHandler } from 'types/form';
import type { FormErrors, GenericFormValues, UseFormChangeHandler, UseFormBlurHandler, UseFormSubmitHandler } from 'types/form';
import { ValidationError, AnySchema } from 'yup';

export type UseFormReturnValue<T> = {
values: T;
errors: FormErrors<T>;
submitting: boolean;
handleChange: UseFormChangeHandler;
handleBlur: UseFormBlurHandler;
handleSubmit: UseFormSubmitHandler;
setValue: (key: keyof T, value: string) => void;
setErrors: (errors: FormErrors<T>) => void;
Expand All @@ -26,19 +27,53 @@ export default function useForm<T extends GenericFormValues>(
initialValues: T,
onSubmit: UseFormOnSubmitHandler<T>,
validationSchema?: AnySchema,
validateOnBlur: boolean = false,
): UseFormReturnValue<T> {
const [touched, setTouched] = useState<Record<keyof T, boolean>>(
Object.fromEntries((Object.keys(initialValues) as Array<keyof T>).map((key) => [key, false])) as Record<keyof T, boolean>,
);
const [values, setValues] = useState<T>(initialValues);
const [submitting, setSubmitting] = useState(false);
const [errors, setErrors] = useState<FormErrors<T>>({});

const validateField = (name: string, formValues: T) => {
if (!validationSchema) return;

try {
validationSchema.validateSyncAt(name, formValues);

// clear error
setErrors((errors) => ({ ...errors, [name]: null }));
} catch (error: unknown) {
if (error instanceof ValidationError) {
const errorMessage = error.errors[0];
setErrors((errors) => ({ ...errors, [name]: errorMessage }));
}
}
};

const setValue = (name: keyof T, value: string | boolean) => {
setValues((current) => ({ ...current, [name]: value }));
};

const handleChange: UseFormChangeHandler = (event) => {
const name = event.target.name;
const value = event.target instanceof HTMLInputElement && event.target.type === 'checkbox' ? event.target.checked : event.target.value;

setValues((current) => ({ ...current, [event.target.name]: value }));
const newValues = { ...values, [name]: value };

setValues(newValues);
setTouched(current => ({ ...current, [name]: value }));

if (errors[name]) {
validateField(name, newValues)
}
};

const handleBlur: UseFormBlurHandler = (event) => {
if (!validateOnBlur || !touched[event.target.name]) return;

validateField(event.target.name, values);
};

const validate = (validationSchema: AnySchema) => {
Expand Down Expand Up @@ -85,5 +120,5 @@ export default function useForm<T extends GenericFormValues>(
onSubmit(values, { setValue, setErrors, setSubmitting, validate });
};

return { values, errors, handleChange, handleSubmit, submitting, setValue, setErrors, setSubmitting };
return { values, errors, handleChange, handleBlur, handleSubmit, submitting, setValue, setErrors, setSubmitting };
}
11 changes: 9 additions & 2 deletions src/i18n/locales/en_US/account.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,16 @@
"field_is_not_valid_email": "Please re-enter your email details",
"field_required": "This field is required",
"hide_password": "Hide password",
"invalid_password": "Use a minimum of 6 characters (case sensitive) with at least one number or special character and one capital character",
"invalid_password": "Use a minimum of 8 characters (case sensitive) with at least one number",
"password": "Password",
"password_strength": "Use a minimum of 6 characters (case sensitive) with at least one number or special character and one capital character",
"password_helper_text": "Use a minimum of 8 characters (case sensitive) with at least one number",
"password_strength": {
"fair": "Fair",
"invalid": "",
"strong": "Strong",
"very_strong": "Very strong",
"weak": "Weak"
},
"sign_up": "Sign up",
"user_exists": "There is already a user with this email address",
"view_password": "View password",
Expand Down
Loading

0 comments on commit 89644bc

Please sign in to comment.