From e1546acb1487c21ff8a7f09ca64a62dc4e1944b2 Mon Sep 17 00:00:00 2001 From: Mubbshar Anwar <78487564+mubbsharanwar@users.noreply.github.com> Date: Thu, 3 Jun 2021 18:21:18 +0500 Subject: [PATCH] Clear field error on focus (#298) Clear field related error messages for all forms on focus in event VAN-505 --- .../AuthnValidationFormGroup.jsx | 166 ------------------ src/common-components/PasswordField.jsx | 11 +- src/common-components/index.jsx | 1 - .../tests/AuthnValidationFormGroup.test.jsx | 46 ----- .../tests/FormField.test.jsx | 2 + .../tests/ForgotPasswordPage.test.jsx | 12 ++ src/login/LoginPage.jsx | 10 +- src/login/tests/LoginPage.test.jsx | 16 +- src/register/RegistrationPage.jsx | 11 ++ src/register/tests/RegistrationPage.test.jsx | 26 +++ src/reset-password/ResetPasswordPage.jsx | 6 + .../tests/ResetPasswordPage.test.jsx | 4 + 12 files changed, 94 insertions(+), 217 deletions(-) delete mode 100644 src/common-components/AuthnValidationFormGroup.jsx delete mode 100644 src/common-components/tests/AuthnValidationFormGroup.test.jsx diff --git a/src/common-components/AuthnValidationFormGroup.jsx b/src/common-components/AuthnValidationFormGroup.jsx deleted file mode 100644 index cc52478c..00000000 --- a/src/common-components/AuthnValidationFormGroup.jsx +++ /dev/null @@ -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 ( - {props.label} - ); - }; - const showOptional = () => { - const additionalField = props.optionalFieldCheckbox ? ( - - ) : ; - 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 ( - - {showLabel()} - - {showOptional()} - - ); -}; - -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; diff --git a/src/common-components/PasswordField.jsx b/src/common-components/PasswordField.jsx index 7484e27e..3a1793f1 100644 --- a/src/common-components/PasswordField.jsx +++ b/src/common-components/PasswordField.jsx @@ -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 = ( ); @@ -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, diff --git a/src/common-components/index.jsx b/src/common-components/index.jsx index a4463735..d57bf808 100644 --- a/src/common-components/index.jsx +++ b/src/common-components/index.jsx @@ -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'; diff --git a/src/common-components/tests/AuthnValidationFormGroup.test.jsx b/src/common-components/tests/AuthnValidationFormGroup.test.jsx deleted file mode 100644 index 0c765e54..00000000 --- a/src/common-components/tests/AuthnValidationFormGroup.test.jsx +++ /dev/null @@ -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(); - 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(); - - 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(); - 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(); - expect(validationFormGroup.find('label').prop('className')).toEqual('pgn__form-label sr-only'); - }); -}); diff --git a/src/common-components/tests/FormField.test.jsx b/src/common-components/tests/FormField.test.jsx index 7cf0c918..c78678d8 100644 --- a/src/common-components/tests/FormField.test.jsx +++ b/src/common-components/tests/FormField.test.jsx @@ -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(), }; }); diff --git a/src/forgot-password/tests/ForgotPasswordPage.test.jsx b/src/forgot-password/tests/ForgotPasswordPage.test.jsx index aee861ee..1445bf4e 100644 --- a/src/forgot-password/tests/ForgotPasswordPage.test.jsx +++ b/src/forgot-password/tests/ForgotPasswordPage.test.jsx @@ -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()); + 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()); expect(forgotPage.find()).toBeTruthy(); diff --git a/src/login/LoginPage.jsx b/src/login/LoginPage.jsx index a04371ea..59aaee2c 100644 --- a/src/login/LoginPage.jsx +++ b/src/login/LoginPage.jsx @@ -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 ? : null}
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'])} /> diff --git a/src/login/tests/LoginPage.test.jsx b/src/login/tests/LoginPage.test.jsx index ba1dd355..7b51b5d5 100644 --- a/src/login/tests/LoginPage.test.jsx +++ b/src/login/tests/LoginPage.test.jsx @@ -87,7 +87,7 @@ describe('LoginPage', () => { store.dispatch = jest.fn(store.dispatch); const loginPage = mount(reduxWrapper()); - 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())).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())).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', () => { diff --git a/src/register/RegistrationPage.jsx b/src/register/RegistrationPage.jsx index 7afd6ab1..3f379b07 100644 --- a/src/register/RegistrationPage.jsx +++ b/src/register/RegistrationPage.jsx @@ -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={} diff --git a/src/register/tests/RegistrationPage.test.jsx b/src/register/tests/RegistrationPage.test.jsx index 8463a0c9..a1ee43e2 100644 --- a/src/register/tests/RegistrationPage.test.jsx +++ b/src/register/tests/RegistrationPage.test.jsx @@ -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()); + 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', () => { diff --git a/src/reset-password/ResetPasswordPage.jsx b/src/reset-password/ResetPasswordPage.jsx index 13aae5f3..65565077 100644 --- a/src/reset-password/ResetPasswordPage.jsx +++ b/src/reset-password/ResetPasswordPage.jsx @@ -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'])} diff --git a/src/reset-password/tests/ResetPasswordPage.test.jsx b/src/reset-password/tests/ResetPasswordPage.test.jsx index cb3cf6e0..83ba02eb 100644 --- a/src/reset-password/tests/ResetPasswordPage.test.jsx +++ b/src/reset-password/tests/ResetPasswordPage.test.jsx @@ -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', () => {