Clear field error on focus (#298)
Clear field related error messages for all forms on focus in event VAN-505
This commit is contained in:
committed by
Waheed Ahmed
parent
92163ac7dc
commit
e1546acb14
@@ -1,166 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Form,
|
||||
Input,
|
||||
ValidationFormGroup,
|
||||
} from '@edx/paragon';
|
||||
|
||||
const AuthnCustomValidationFormGroup = (props) => {
|
||||
const {
|
||||
onBlur, onChange, onClick, onFocus,
|
||||
} = props;
|
||||
const [showHelpText, setShowHelpText] = useState(false);
|
||||
const [showLabelText, setShowLabelText] = useState(false);
|
||||
|
||||
// handler code that need to be invoked via input
|
||||
const onClickHandler = (e, clickCb) => {
|
||||
setShowHelpText(true);
|
||||
setShowLabelText(true);
|
||||
if (clickCb) {
|
||||
clickCb(e);
|
||||
}
|
||||
};
|
||||
const onBlurHandler = (e, blurCb) => {
|
||||
setShowHelpText(false);
|
||||
setShowLabelText(false);
|
||||
if (blurCb) {
|
||||
blurCb(e);
|
||||
}
|
||||
};
|
||||
const onChangeHandler = (e, changeCb) => {
|
||||
if (changeCb) {
|
||||
changeCb(e);
|
||||
}
|
||||
};
|
||||
const onFocusHandler = (e, focusCb) => {
|
||||
if (focusCb) {
|
||||
focusCb(e);
|
||||
}
|
||||
};
|
||||
const onOptionalHandler = (e, clickCb) => { clickCb(e); };
|
||||
|
||||
const showLabel = () => {
|
||||
let className;
|
||||
if (props.optionalFieldCheckbox || (!showLabelText && (props.value !== '' || props.type === 'select'))) {
|
||||
className = 'sr-only';
|
||||
} else if (showLabelText) {
|
||||
className = 'pt-10 h6';
|
||||
} else {
|
||||
className = 'pt-10 focus-out';
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.Label htmlFor={props.for} className={className}>{props.label}</Form.Label>
|
||||
);
|
||||
};
|
||||
const showOptional = () => {
|
||||
const additionalField = props.optionalFieldCheckbox ? (
|
||||
<p role="presentation" id="additionalFields" className="mb-1 small" onClick={(e) => onOptionalHandler(e, onClick)}>
|
||||
{props.checkboxMessage}
|
||||
</p>
|
||||
) : <span />;
|
||||
return additionalField;
|
||||
};
|
||||
|
||||
const inputProps = {
|
||||
name: props.name,
|
||||
id: props.for,
|
||||
type: props.type,
|
||||
value: props.value,
|
||||
className: props.inputFieldStyle,
|
||||
'aria-invalid': props.ariaInvalid,
|
||||
autoComplete: 'on',
|
||||
};
|
||||
inputProps.onChange = (e) => onChangeHandler(e, onChange);
|
||||
inputProps.onClick = (e) => onClickHandler(e, onClick);
|
||||
inputProps.onBlur = (e) => onBlurHandler(e, onBlur);
|
||||
inputProps.onFocus = (e) => onFocusHandler(e, onFocus);
|
||||
|
||||
if (props.type === 'select') {
|
||||
inputProps.options = props.selectOptions;
|
||||
inputProps.className = props.value === '' ? `${props.inputFieldStyle} text-muted` : props.inputFieldStyle;
|
||||
}
|
||||
if (props.type === 'checkbox') {
|
||||
inputProps.checked = props.isChecked;
|
||||
}
|
||||
|
||||
const validationGroupProps = {
|
||||
for: props.for,
|
||||
};
|
||||
if (!props.optionalFieldCheckbox) {
|
||||
validationGroupProps.invalid = props.invalid;
|
||||
validationGroupProps.invalidMessage = props.invalidMessage;
|
||||
validationGroupProps.helpText = showHelpText ? props.helpText : '';
|
||||
} else {
|
||||
validationGroupProps.className = props.optionalFieldCheckbox ? 'custom-control pt-10 mb-0' : '';
|
||||
}
|
||||
if (props.className) {
|
||||
validationGroupProps.className = props.className;
|
||||
}
|
||||
|
||||
return (
|
||||
<ValidationFormGroup
|
||||
{...validationGroupProps}
|
||||
>
|
||||
{showLabel()}
|
||||
<Input
|
||||
{...inputProps}
|
||||
required
|
||||
/>
|
||||
{showOptional()}
|
||||
</ValidationFormGroup>
|
||||
);
|
||||
};
|
||||
|
||||
AuthnCustomValidationFormGroup.defaultProps = {
|
||||
name: '',
|
||||
for: '',
|
||||
label: '',
|
||||
optionalFieldCheckbox: false,
|
||||
type: '',
|
||||
value: '',
|
||||
invalid: false,
|
||||
ariaInvalid: false,
|
||||
invalidMessage: '',
|
||||
inputFieldStyle: '',
|
||||
helpText: '',
|
||||
className: '',
|
||||
onClick: null,
|
||||
onBlur: null,
|
||||
onChange: null,
|
||||
onFocus: null,
|
||||
isChecked: false,
|
||||
checkboxMessage: '',
|
||||
selectOptions: null,
|
||||
};
|
||||
|
||||
AuthnCustomValidationFormGroup.propTypes = {
|
||||
name: PropTypes.string,
|
||||
for: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.bool,
|
||||
]),
|
||||
invalid: PropTypes.bool,
|
||||
ariaInvalid: PropTypes.bool,
|
||||
invalidMessage: PropTypes.string,
|
||||
helpText: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
inputFieldStyle: PropTypes.string,
|
||||
isChecked: PropTypes.bool,
|
||||
optionalFieldCheckbox: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
onBlur: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
onFocus: PropTypes.func,
|
||||
checkboxMessage: PropTypes.string,
|
||||
selectOptions: PropTypes.arrayOf(PropTypes.shape({
|
||||
key: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
})),
|
||||
};
|
||||
|
||||
export default AuthnCustomValidationFormGroup;
|
||||
@@ -21,6 +21,13 @@ const PasswordField = (props) => {
|
||||
setShowTooltip(props.showRequirements && false);
|
||||
};
|
||||
|
||||
const handleFocus = (e) => {
|
||||
if (props.handleFocus) {
|
||||
props.handleFocus(e);
|
||||
}
|
||||
setTimeout(() => setShowTooltip(props.showRequirements && true), 150);
|
||||
};
|
||||
|
||||
const HideButton = (
|
||||
<IconButton icon={faEyeSlash} onClick={setHiddenTrue} alt={formatMessage(messages['hide.password'])} aria-label={formatMessage(messages['hide.password'])} />
|
||||
);
|
||||
@@ -54,7 +61,7 @@ const PasswordField = (props) => {
|
||||
type={isPasswordHidden ? 'password' : 'text'}
|
||||
name={props.name}
|
||||
value={props.value}
|
||||
onFocus={() => setTimeout(() => setShowTooltip(props.showRequirements && true), 150)}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
onChange={props.handleChange}
|
||||
controlClassName={props.borderClass}
|
||||
@@ -73,6 +80,7 @@ PasswordField.defaultProps = {
|
||||
borderClass: '',
|
||||
errorMessage: '',
|
||||
handleBlur: null,
|
||||
handleFocus: null,
|
||||
handleChange: () => {},
|
||||
showRequirements: true,
|
||||
};
|
||||
@@ -82,6 +90,7 @@ PasswordField.propTypes = {
|
||||
errorMessage: PropTypes.string,
|
||||
floatingLabel: PropTypes.string.isRequired,
|
||||
handleBlur: PropTypes.func,
|
||||
handleFocus: PropTypes.func,
|
||||
handleChange: PropTypes.func,
|
||||
intl: intlShape.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
|
||||
@@ -7,7 +7,6 @@ export { default as SocialAuthProviders } from './SocialAuthProviders';
|
||||
export { default as ThirdPartyAuthAlert } from './ThirdPartyAuthAlert';
|
||||
export { default as InstitutionLogistration } from './InstitutionLogistration';
|
||||
export { RenderInstitutionButton } from './InstitutionLogistration';
|
||||
export { default as AuthnValidationFormGroup } from './AuthnValidationFormGroup';
|
||||
export { default as APIFailureMessage } from './APIFailureMessage';
|
||||
export { default as reducer } from './data/reducers';
|
||||
export { default as saga } from './data/sagas';
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import AuthnCustomValidationFormGroup from '../AuthnValidationFormGroup';
|
||||
|
||||
describe('AuthnCustomValidationFormGroup', () => {
|
||||
let props = {
|
||||
label: 'Email Label',
|
||||
for: 'email',
|
||||
name: 'email',
|
||||
type: 'email',
|
||||
value: '',
|
||||
helpText: 'Email field help text',
|
||||
};
|
||||
|
||||
it('should show label in place of placeholder when field is empty', () => {
|
||||
const validationFormGroup = mount(<AuthnCustomValidationFormGroup {...props} />);
|
||||
expect(validationFormGroup.find('label').prop('className')).toEqual('pgn__form-label pt-10 focus-out');
|
||||
});
|
||||
|
||||
it('should show label on top of field when field is focused in', () => {
|
||||
const validationFormGroup = mount(<AuthnCustomValidationFormGroup {...props} />);
|
||||
|
||||
validationFormGroup.find('input').simulate('focus');
|
||||
expect(validationFormGroup.find('label').prop('className')).toEqual('pgn__form-label pt-10 h6');
|
||||
});
|
||||
|
||||
it('should keep label hidden for checkbox field', () => {
|
||||
props = {
|
||||
...props,
|
||||
type: 'checkbox',
|
||||
optionalFieldCheckbox: true,
|
||||
};
|
||||
const validationFormGroup = mount(<AuthnCustomValidationFormGroup {...props} />);
|
||||
expect(validationFormGroup.find('label').prop('className')).toEqual('pgn__form-label sr-only');
|
||||
});
|
||||
|
||||
it('should keep label hidden when input field is not empty', () => {
|
||||
props = {
|
||||
...props,
|
||||
value: 'test',
|
||||
};
|
||||
const validationFormGroup = mount(<AuthnCustomValidationFormGroup {...props} />);
|
||||
expect(validationFormGroup.find('label').prop('className')).toEqual('pgn__form-label sr-only');
|
||||
});
|
||||
});
|
||||
@@ -13,6 +13,7 @@ describe('FormGroup', () => {
|
||||
helpText: ['Email field help text'],
|
||||
name: 'email',
|
||||
value: '',
|
||||
handleFocus: jest.fn(),
|
||||
};
|
||||
|
||||
it('should show help text on field focus', () => {
|
||||
@@ -33,6 +34,7 @@ describe('PasswordField', () => {
|
||||
floatingLabel: 'Password',
|
||||
name: 'password',
|
||||
value: 'password123',
|
||||
handleFocus: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -121,6 +121,18 @@ describe('ForgotPasswordPage', () => {
|
||||
expect(forgotPasswordPage.find('.pgn__form-text-invalid').text()).toEqual(validationMessage);
|
||||
});
|
||||
|
||||
it('should clear error message on focus event', async () => {
|
||||
const validationMessage = 'Enter your email';
|
||||
const forgotPasswordPage = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
await act(async () => { await forgotPasswordPage.find('button.btn-brand').simulate('click'); });
|
||||
|
||||
forgotPasswordPage.update();
|
||||
expect(forgotPasswordPage.find('.pgn__form-text-invalid').text()).toEqual(validationMessage);
|
||||
|
||||
forgotPasswordPage.find('input#email').simulate('focus');
|
||||
expect(forgotPasswordPage.find('#email-invalid-feedback').exists()).toEqual(false);
|
||||
});
|
||||
|
||||
it('check cookie rendered', () => {
|
||||
const forgotPage = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
expect(forgotPage.find(<CookiePolicyBanner />)).toBeTruthy();
|
||||
|
||||
@@ -97,6 +97,12 @@ class LoginPage extends React.Component {
|
||||
this.props.loginRequest(payload);
|
||||
}
|
||||
|
||||
handleOnFocus = (e) => {
|
||||
const { errors } = this.state;
|
||||
errors[e.target.name] = '';
|
||||
this.setState({ errors });
|
||||
}
|
||||
|
||||
validateEmail(email) {
|
||||
const { errors } = this.state;
|
||||
|
||||
@@ -210,9 +216,10 @@ class LoginPage extends React.Component {
|
||||
{this.props.resetPassword && !this.props.loginError ? <ResetPasswordSuccess /> : null}
|
||||
<Form>
|
||||
<FormGroup
|
||||
name="email"
|
||||
name="emailOrUsername"
|
||||
value={this.state.emailOrUsername}
|
||||
handleChange={(e) => this.setState({ emailOrUsername: e.target.value, isSubmitted: false })}
|
||||
handleFocus={this.handleOnFocus}
|
||||
errorMessage={this.state.errors.emailOrUsername}
|
||||
floatingLabel={intl.formatMessage(messages['login.user.identity.label'])}
|
||||
/>
|
||||
@@ -221,6 +228,7 @@ class LoginPage extends React.Component {
|
||||
value={this.state.password}
|
||||
showRequirements={false}
|
||||
handleChange={(e) => this.setState({ password: e.target.value, isSubmitted: false })}
|
||||
handleFocus={this.handleOnFocus}
|
||||
errorMessage={this.state.errors.password}
|
||||
floatingLabel={intl.formatMessage(messages['login.password.label'])}
|
||||
/>
|
||||
|
||||
@@ -87,7 +87,7 @@ describe('LoginPage', () => {
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
|
||||
loginPage.find('input#email').simulate('change', { target: { value: 'test@example.com' } });
|
||||
loginPage.find('input#emailOrUsername').simulate('change', { target: { value: 'test@example.com' } });
|
||||
loginPage.find('input#password').simulate('change', { target: { value: 'password' } });
|
||||
loginPage.find('button.btn-brand').simulate('click');
|
||||
|
||||
@@ -123,12 +123,24 @@ describe('LoginPage', () => {
|
||||
const loginPage = (mount(reduxWrapper(<IntlLoginPage {...props} />))).find('LoginPage');
|
||||
|
||||
loginPage.find('input#password').simulate('change', { target: { value: 'test', name: 'password' } });
|
||||
loginPage.find('input#email').simulate('change', { target: { value: 'te', name: 'email' } });
|
||||
loginPage.find('input#emailOrUsername').simulate('change', { target: { value: 'te', name: 'email' } });
|
||||
loginPage.find('button.btn-brand').simulate('click');
|
||||
|
||||
expect(loginPage.state('errors')).toEqual(errorState);
|
||||
});
|
||||
|
||||
it('should reset field related error messages on onFocus event', () => {
|
||||
const errorState = { emailOrUsername: '', password: '' };
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
const loginPage = (mount(reduxWrapper(<IntlLoginPage {...props} />))).find('LoginPage');
|
||||
loginPage.find('button.btn-brand').simulate('click');
|
||||
|
||||
loginPage.find('input#emailOrUsername').simulate('focus');
|
||||
loginPage.find('input#password').simulate('focus');
|
||||
expect(loginPage.state('errors')).toEqual(errorState);
|
||||
});
|
||||
|
||||
// ******** test form buttons and links ********
|
||||
|
||||
it('should match default button state', () => {
|
||||
|
||||
@@ -225,6 +225,12 @@ class RegistrationPage extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleOnFocus = (e) => {
|
||||
const { errors } = this.state;
|
||||
errors[e.target.name] = '';
|
||||
this.setState({ errors });
|
||||
}
|
||||
|
||||
handleSuggestionClick = (suggestion) => {
|
||||
const { errors } = this.state;
|
||||
errors.username = '';
|
||||
@@ -451,6 +457,7 @@ class RegistrationPage extends React.Component {
|
||||
value={this.state.name}
|
||||
handleBlur={this.handleOnBlur}
|
||||
handleChange={this.handleOnChange}
|
||||
handleFocus={this.handleOnFocus}
|
||||
errorMessage={this.state.errors.name}
|
||||
floatingLabel={intl.formatMessage(messages['registration.fullname.label'])}
|
||||
/>
|
||||
@@ -459,6 +466,7 @@ class RegistrationPage extends React.Component {
|
||||
value={this.state.username}
|
||||
handleBlur={this.handleOnBlur}
|
||||
handleChange={this.handleOnChange}
|
||||
handleFocus={this.handleOnFocus}
|
||||
errorMessage={this.state.errors.username}
|
||||
helpText={[intl.formatMessage(messages['help.text.username.1']), intl.formatMessage(messages['help.text.username.2'])]}
|
||||
floatingLabel={intl.formatMessage(messages['registration.username.label'])}
|
||||
@@ -471,6 +479,7 @@ class RegistrationPage extends React.Component {
|
||||
handleBlur={this.handleOnBlur}
|
||||
handleChange={this.handleOnChange}
|
||||
errorMessage={this.state.errors.email}
|
||||
handleFocus={this.handleOnFocus}
|
||||
helpText={[intl.formatMessage(messages['help.text.email'])]}
|
||||
floatingLabel={intl.formatMessage(messages['registration.email.label'])}
|
||||
borderClass={this.state.borderClass}
|
||||
@@ -484,6 +493,7 @@ class RegistrationPage extends React.Component {
|
||||
value={this.state.password}
|
||||
handleBlur={this.handleOnBlur}
|
||||
handleChange={this.handleOnChange}
|
||||
handleFocus={this.handleOnFocus}
|
||||
errorMessage={this.state.errors.password}
|
||||
floatingLabel={intl.formatMessage(messages['registration.password.label'])}
|
||||
/>
|
||||
@@ -494,6 +504,7 @@ class RegistrationPage extends React.Component {
|
||||
value={this.state.country}
|
||||
handleBlur={this.handleOnBlur}
|
||||
handleChange={this.handleOnChange}
|
||||
handleFocus={this.handleOnFocus}
|
||||
errorMessage={this.state.errors.country}
|
||||
floatingLabel={intl.formatMessage(messages['registration.country.label'])}
|
||||
trailingElement={<Icon src={ExpandMore} />}
|
||||
|
||||
@@ -293,6 +293,32 @@ describe('RegistrationPage', () => {
|
||||
expect(registrationPage.state('errorCode')).toEqual('duplicate-username');
|
||||
});
|
||||
|
||||
// ******** test clear error messages on focus in ********
|
||||
|
||||
it('should clear field related error messages on input field Focus', () => {
|
||||
const errors = {
|
||||
email: '',
|
||||
name: '',
|
||||
username: '',
|
||||
password: '',
|
||||
country: '',
|
||||
};
|
||||
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
registrationPage.find('button.btn-brand').simulate('click');
|
||||
|
||||
expect(registrationPage.find('div[feedback-for="name"]').text()).toEqual(emptyFieldValidation.name);
|
||||
registrationPage.find('input#name').simulate('focus');
|
||||
expect(registrationPage.find('div[feedback-for="username"]').text()).toEqual(emptyFieldValidation.username);
|
||||
registrationPage.find('input#username').simulate('focus');
|
||||
expect(registrationPage.find('div[feedback-for="email"]').text()).toEqual(emptyFieldValidation.email);
|
||||
registrationPage.find('input#email').simulate('focus');
|
||||
expect(registrationPage.find('div[feedback-for="password"]').text()).toEqual(emptyFieldValidation.password);
|
||||
registrationPage.find('input#password').simulate('focus');
|
||||
expect(registrationPage.find('div[feedback-for="country"]').text()).toEqual(emptyFieldValidation.country);
|
||||
registrationPage.find('select#country').simulate('blur', { target: { value: 'US', name: 'country' } });
|
||||
expect(registrationPage.find('RegistrationPage').state('errors')).toEqual(errors);
|
||||
});
|
||||
|
||||
// ******** test alert messages ********
|
||||
|
||||
it('should match third party auth alert', () => {
|
||||
|
||||
@@ -84,6 +84,10 @@ const ResetPasswordPage = (props) => {
|
||||
validateInput('confirmPassword', value);
|
||||
};
|
||||
|
||||
const handleOnFocus = (e) => {
|
||||
setFormErrors({ [e.target.name]: '' });
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -137,6 +141,7 @@ const ResetPasswordPage = (props) => {
|
||||
value={newPassword}
|
||||
handleChange={(e) => setNewPassword(e.target.value)}
|
||||
handleBlur={(e) => validateInput(e.target.name, e.target.value)}
|
||||
handleFocus={handleOnFocus}
|
||||
errorMessage={formErrors.newPassword}
|
||||
floatingLabel={intl.formatMessage(messages['new.password.label'])}
|
||||
/>
|
||||
@@ -144,6 +149,7 @@ const ResetPasswordPage = (props) => {
|
||||
name="confirmPassword"
|
||||
value={confirmPassword}
|
||||
handleChange={handleConfirmPasswordChange}
|
||||
handleFocus={handleOnFocus}
|
||||
errorMessage={formErrors.confirmPassword}
|
||||
showRequirements={false}
|
||||
floatingLabel={intl.formatMessage(messages['confirm.password.label'])}
|
||||
|
||||
@@ -99,6 +99,10 @@ describe('ResetPasswordPage', () => {
|
||||
);
|
||||
expect(resetPasswordPage.find('div[feedback-for="newPassword"]').text()).toEqual('Password criteria has not been met');
|
||||
expect(resetPasswordPage.find('div[feedback-for="confirmPassword"]').text()).toEqual('Confirm your password');
|
||||
resetPasswordPage.find('input#newPassword').simulate('focus');
|
||||
expect(resetPasswordPage.find('div[feedback-for="newPassword"]').exists()).toBe(false);
|
||||
resetPasswordPage.find('input#confirmPassword').simulate('focus');
|
||||
expect(resetPasswordPage.find('div[feedback-for="confirmPassword"]').exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should show error message when new and confirm password do not match', () => {
|
||||
|
||||
Reference in New Issue
Block a user