fix: VAN-1047 - Handled country field's errors and state persistence (#640)
This commit is contained in:
@@ -6,31 +6,51 @@ import PropTypes from 'prop-types';
|
||||
import onClickOutside from 'react-onclickoutside';
|
||||
|
||||
import { FormGroup } from '../common-components';
|
||||
import { FORM_SUBMISSION_ERROR } from './data/constants';
|
||||
import { COUNTRY_CODE_KEY, COUNTRY_DISPLAY_KEY, FORM_SUBMISSION_ERROR } from './data/constants';
|
||||
|
||||
class CountryDropdown extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleFocus = this.handleFocus.bind(this);
|
||||
this.handleOnBlur = this.handleOnBlur.bind(this);
|
||||
|
||||
this.state = {
|
||||
displayValue: '',
|
||||
icon: this.expandMoreButton(),
|
||||
errorMessage: '',
|
||||
showFieldError: true,
|
||||
};
|
||||
|
||||
this.handleFocus = this.handleFocus.bind(this);
|
||||
this.handleOnBlur = this.handleOnBlur.bind(this);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
if (this.props.value !== nextProps.value && nextProps.value !== null) {
|
||||
const opt = this.props.options.find((o) => o[this.props.valueKey] === nextProps.value);
|
||||
if (opt && opt[this.props.displayValueKey] !== this.state.displayValue) {
|
||||
this.setState({ displayValue: opt[this.props.displayValueKey], showFieldError: false });
|
||||
const selectedCountry = this.props.options.find((o) => o[COUNTRY_CODE_KEY] === nextProps.value);
|
||||
if (this.props.value !== nextProps.value) {
|
||||
if (selectedCountry) {
|
||||
this.setState({
|
||||
displayValue: selectedCountry[COUNTRY_DISPLAY_KEY],
|
||||
showFieldError: false,
|
||||
errorMessage: nextProps.errorMessage,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
// Set persisted country value as display value.
|
||||
this.setState({ displayValue: nextProps.value, showFieldError: true, errorMessage: nextProps.errorMessage });
|
||||
return false;
|
||||
// eslint-disable-next-line no-else-return
|
||||
} else if (nextProps.value && selectedCountry && this.state.displayValue === nextProps.value) {
|
||||
// Handling a case here where user enters a valid country code that needs to be
|
||||
// evaluated and set its display value.
|
||||
this.setState({ displayValue: selectedCountry[COUNTRY_DISPLAY_KEY] });
|
||||
return false;
|
||||
}
|
||||
if (this.props.errorCode !== nextProps.errorCode && nextProps.errorCode === 'invalid-country') {
|
||||
this.setState({ showFieldError: true, errorMessage: nextProps.errorMessage });
|
||||
return false;
|
||||
}
|
||||
if (this.state.errorMessage !== nextProps.errorMessage) {
|
||||
this.setState({ showFieldError: true, errorMessage: nextProps.errorMessage });
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -48,72 +68,30 @@ class CountryDropdown extends React.Component {
|
||||
}
|
||||
|
||||
return options.map((opt) => {
|
||||
const value = opt[this.props.valueKey];
|
||||
let displayValue = opt[this.props.displayValueKey];
|
||||
if (displayValue.length > 30) {
|
||||
displayValue = displayValue.substring(0, 30).concat('...');
|
||||
}
|
||||
const value = opt[COUNTRY_CODE_KEY];
|
||||
const displayValue = opt[COUNTRY_DISPLAY_KEY];
|
||||
|
||||
return (
|
||||
<button type="button" className="dropdown-item data-hj-suppress" value={value} key={value} onClick={(e) => { this.handleItemClick(e); }}>
|
||||
{displayValue}
|
||||
<button type="button" name="countryItem" className="dropdown-item data-hj-suppress" value={displayValue} key={value} onClick={(e) => { this.handleItemClick(e); }}>
|
||||
{displayValue.length > 30 ? displayValue.substring(0, 30).concat('...') : displayValue}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
setValue(value) {
|
||||
if (this.props.value === value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.props.handleChange) {
|
||||
this.props.handleChange(value);
|
||||
}
|
||||
|
||||
const opt = this.props.options.find((o) => o[this.props.valueKey] === value);
|
||||
if (opt && opt[this.props.displayValueKey] !== this.state.displayValue) {
|
||||
this.setState({ displayValue: opt[this.props.displayValueKey], showFieldError: false });
|
||||
}
|
||||
}
|
||||
|
||||
setDisplayValue(value) {
|
||||
const normalized = value.toLowerCase();
|
||||
const opt = this.props.options.find((o) => o[this.props.displayValueKey].toLowerCase() === normalized);
|
||||
if (opt) {
|
||||
this.setValue(opt[this.props.valueKey]);
|
||||
this.setState({ displayValue: opt[this.props.displayValueKey], showFieldError: false });
|
||||
} else {
|
||||
this.setValue(null);
|
||||
this.setState({ displayValue: value, showFieldError: true });
|
||||
}
|
||||
}
|
||||
|
||||
handleClick = (e) => {
|
||||
const dropDownItems = this.getItems(e.target.value);
|
||||
if (dropDownItems?.length > 1) {
|
||||
this.setState({ dropDownItems, icon: this.expandLessButton(), errorMessage: '' });
|
||||
}
|
||||
|
||||
if (this.state.dropDownItems?.length > 0) {
|
||||
this.setState({ dropDownItems: '', icon: this.expandMoreButton(), errorMessage: '' });
|
||||
}
|
||||
}
|
||||
|
||||
handleOnChange = (e) => {
|
||||
const filteredItems = this.getItems(e.target.value);
|
||||
|
||||
this.setState({ dropDownItems: filteredItems, icon: this.expandLessButton(), errorMessage: '' });
|
||||
this.setDisplayValue(e.target.value);
|
||||
this.setState({
|
||||
dropDownItems: filteredItems,
|
||||
displayValue: e.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
handleClickOutside = () => {
|
||||
if (this.state.dropDownItems?.length > 0) {
|
||||
const msg = this.state.displayValue === '' ? this.props.errorMessage : '';
|
||||
this.setState(() => ({
|
||||
icon: this.expandMoreButton(),
|
||||
dropDownItems: '',
|
||||
errorMessage: msg,
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -122,32 +100,51 @@ class CountryDropdown extends React.Component {
|
||||
this.setState({ dropDownItems: '', icon: this.expandMoreButton() });
|
||||
}
|
||||
|
||||
handleExpandMore(e) {
|
||||
const dropDownItems = this.getItems(e.target.value);
|
||||
this.setState({
|
||||
dropDownItems, icon: this.expandLessButton(), errorMessage: '', showFieldError: false,
|
||||
});
|
||||
handleExpandMore() {
|
||||
this.setState(prevState => ({
|
||||
dropDownItems: this.getItems(prevState.displayValue), icon: this.expandLessButton(), errorMessage: '', showFieldError: false,
|
||||
}));
|
||||
}
|
||||
|
||||
handleFocus(e) {
|
||||
this.setState({ showFieldError: false });
|
||||
const { name, value } = e.target;
|
||||
this.setState(prevState => ({
|
||||
dropDownItems: this.getItems(name === 'country' ? value : prevState.displayValue),
|
||||
icon: this.expandLessButton(),
|
||||
errorMessage: '',
|
||||
showFieldError: false,
|
||||
}));
|
||||
if (this.props.handleFocus) { this.props.handleFocus(e); }
|
||||
}
|
||||
|
||||
handleOnBlur(e) {
|
||||
if (this.props.handleBlur) { this.props.handleBlur(e); }
|
||||
handleOnBlur(e, itemClicked = false) {
|
||||
const { name } = e.target;
|
||||
const countryValue = itemClicked ? e.target.value : this.state.displayValue;
|
||||
// For a better user experience, do not validate when focus out from 'country' field
|
||||
// and focus on 'countryItem' or 'countryExpand' button.
|
||||
if (e.relatedTarget && e.relatedTarget.name === 'countryItem' && (name === 'country' || name === 'countryExpand')) {
|
||||
return;
|
||||
}
|
||||
const normalized = countryValue.toLowerCase();
|
||||
const selectedCountry = this.props.options.find((o) => o[COUNTRY_DISPLAY_KEY].toLowerCase() === normalized);
|
||||
if (!selectedCountry) {
|
||||
this.setState({ errorMessage: this.props.errorMessage, showFieldError: true });
|
||||
}
|
||||
if (this.props.handleBlur) { this.props.handleBlur({ target: { name: 'country', value: countryValue } }); }
|
||||
}
|
||||
|
||||
handleItemClick(e) {
|
||||
this.setValue(e.target.value);
|
||||
this.setState({ dropDownItems: '', icon: this.expandMoreButton() });
|
||||
this.handleOnBlur(e);
|
||||
this.handleOnBlur(e, true);
|
||||
}
|
||||
|
||||
expandMoreButton() {
|
||||
return (
|
||||
<IconButton
|
||||
className="expand-more"
|
||||
onFocus={this.handleFocus}
|
||||
onBlur={this.handleOnBlur}
|
||||
name="countryExpand"
|
||||
src={ExpandMore}
|
||||
iconAs={Icon}
|
||||
size="sm"
|
||||
@@ -162,6 +159,9 @@ class CountryDropdown extends React.Component {
|
||||
return (
|
||||
<IconButton
|
||||
className="expand-less"
|
||||
onFocus={this.handleFocus}
|
||||
onBlur={this.handleOnBlur}
|
||||
name="countryExpand"
|
||||
src={ExpandLess}
|
||||
iconAs={Icon}
|
||||
size="sm"
|
||||
@@ -178,13 +178,11 @@ class CountryDropdown extends React.Component {
|
||||
<FormGroup
|
||||
as="input"
|
||||
name={this.props.name}
|
||||
readOnly={this.props.readOnly}
|
||||
autoComplete="chrome-off"
|
||||
className="mb-0"
|
||||
floatingLabel={this.props.floatingLabel}
|
||||
trailingElement={this.state.icon}
|
||||
handleChange={this.handleOnChange}
|
||||
handleClick={this.handleClick}
|
||||
handleBlur={this.handleOnBlur}
|
||||
handleFocus={this.handleFocus}
|
||||
value={this.state.displayValue}
|
||||
@@ -202,27 +200,21 @@ CountryDropdown.defaultProps = {
|
||||
options: null,
|
||||
floatingLabel: null,
|
||||
handleFocus: null,
|
||||
handleChange: null,
|
||||
handleBlur: null,
|
||||
value: null,
|
||||
errorMessage: null,
|
||||
errorCode: null,
|
||||
readOnly: false,
|
||||
};
|
||||
|
||||
CountryDropdown.propTypes = {
|
||||
options: PropTypes.arrayOf(PropTypes.object),
|
||||
floatingLabel: PropTypes.string,
|
||||
valueKey: PropTypes.string.isRequired,
|
||||
displayValueKey: PropTypes.string.isRequired,
|
||||
handleFocus: PropTypes.func,
|
||||
handleChange: PropTypes.func,
|
||||
handleBlur: PropTypes.func,
|
||||
value: PropTypes.string,
|
||||
errorMessage: PropTypes.string,
|
||||
errorCode: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
readOnly: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default onClickOutside(CountryDropdown);
|
||||
|
||||
@@ -42,7 +42,8 @@ import {
|
||||
setRegistrationFormData,
|
||||
} from './data/actions';
|
||||
import {
|
||||
COMMON_EMAIL_PROVIDERS, DEFAULT_SERVICE_PROVIDER_DOMAINS, DEFAULT_TOP_LEVEL_DOMAINS, FIELDS, FORM_SUBMISSION_ERROR,
|
||||
COMMON_EMAIL_PROVIDERS, COUNTRY_CODE_KEY, COUNTRY_DISPLAY_KEY, DEFAULT_SERVICE_PROVIDER_DOMAINS,
|
||||
DEFAULT_TOP_LEVEL_DOMAINS, FIELDS, FORM_SUBMISSION_ERROR,
|
||||
} from './data/constants';
|
||||
import {
|
||||
registrationErrorSelector,
|
||||
@@ -63,7 +64,7 @@ class RegistrationPage extends React.Component {
|
||||
super(props, context);
|
||||
sendPageEvent('login_and_registration', 'register');
|
||||
this.handleOnClose = this.handleOnClose.bind(this);
|
||||
|
||||
this.countryList = getCountryList(getLocale());
|
||||
this.queryParams = getAllPossibleQueryParam();
|
||||
// TODO: Once we have tested it and ready for openedX we can remove this flag and make the code
|
||||
// permanent part of Authn and remove extra code
|
||||
@@ -91,7 +92,6 @@ class RegistrationPage extends React.Component {
|
||||
failureCount: 0,
|
||||
startTime: Date.now(),
|
||||
totalRegistrationTime: 0,
|
||||
readOnly: true,
|
||||
validatePassword: false,
|
||||
values: {},
|
||||
focusedField: '',
|
||||
@@ -126,9 +126,11 @@ class RegistrationPage extends React.Component {
|
||||
username: nextProps.registrationFormData.username || this.state.username,
|
||||
password: nextProps.registrationFormData.password || this.state.password,
|
||||
};
|
||||
// Do not set focused field's value from redux store to retain entered data in focused field.
|
||||
let { focusedField } = this.state;
|
||||
|
||||
// do not set focused field's value from redux store to retain entered data in focused field\
|
||||
const { focusedField } = this.state;
|
||||
// Exemption for country field's value as we need to set updated value from the store.
|
||||
if (focusedField === 'country') { focusedField = ''; }
|
||||
const { [focusedField]: _, ...registrationData } = { ...nextProps.registrationFormData, ...nextState };
|
||||
this.setState({
|
||||
...registrationData,
|
||||
@@ -322,24 +324,6 @@ class RegistrationPage extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleCountryChange = (value) => {
|
||||
// TODO: Remove this function once error message is passed as props from
|
||||
// RegistrationPage to CountryDropdown component.
|
||||
if (this.props.registrationFormData.errors.country) {
|
||||
this.props.setRegistrationFormData({
|
||||
country: value,
|
||||
errors: {
|
||||
...this.props.registrationFormData.errors,
|
||||
country: '',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.props.setRegistrationFormData({
|
||||
country: value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleOnFocus = (e) => {
|
||||
const fieldName = e.target.name;
|
||||
this.setState({
|
||||
@@ -350,8 +334,8 @@ class RegistrationPage extends React.Component {
|
||||
if (fieldName === 'username') {
|
||||
this.props.clearUsernameSuggestions();
|
||||
}
|
||||
if (fieldName === 'country') {
|
||||
this.setState({ readOnly: false });
|
||||
if (fieldName === 'countryExpand') {
|
||||
errors.country = '';
|
||||
}
|
||||
if (fieldName === 'passwordValidation') {
|
||||
errors.password = '';
|
||||
@@ -530,23 +514,35 @@ class RegistrationPage extends React.Component {
|
||||
}
|
||||
break;
|
||||
case 'country':
|
||||
if (!value.trim()) {
|
||||
errors.country = intl.formatMessage(messages['empty.country.field.error']);
|
||||
} else {
|
||||
errors.country = '';
|
||||
value = value.trim(); // eslint-disable-line no-param-reassign
|
||||
if (value) {
|
||||
const normalizedValue = value.toLowerCase();
|
||||
let selectedCountry = this.countryList.find((o) => o[COUNTRY_DISPLAY_KEY].toLowerCase() === normalizedValue);
|
||||
if (selectedCountry) {
|
||||
value = selectedCountry[COUNTRY_CODE_KEY]; // eslint-disable-line no-param-reassign
|
||||
errors.country = '';
|
||||
break;
|
||||
} else {
|
||||
// Handling a case here where user enters a valid country code that needs to be
|
||||
// evaluated and set its value as a valid value.
|
||||
selectedCountry = this.countryList.find((o) => o[COUNTRY_CODE_KEY].toLowerCase() === normalizedValue);
|
||||
if (selectedCountry) {
|
||||
value = selectedCountry[COUNTRY_CODE_KEY]; // eslint-disable-line no-param-reassign
|
||||
errors.country = '';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
errors.country = intl.formatMessage(messages['empty.country.field.error']);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (fieldName !== 'country') {
|
||||
state = {
|
||||
...state,
|
||||
[fieldName]: value,
|
||||
};
|
||||
}
|
||||
|
||||
state = {
|
||||
...state,
|
||||
[fieldName]: value,
|
||||
};
|
||||
this.props.setRegistrationFormData({
|
||||
...state,
|
||||
errors,
|
||||
@@ -674,9 +670,7 @@ class RegistrationPage extends React.Component {
|
||||
<CountryDropdown
|
||||
name="country"
|
||||
floatingLabel={intl.formatMessage(messages['registration.country.label'])}
|
||||
options={getCountryList(getLocale())}
|
||||
valueKey="code"
|
||||
displayValueKey="name"
|
||||
options={this.countryList}
|
||||
value={this.state.values[fieldData.name]}
|
||||
handleBlur={this.handleOnBlur}
|
||||
handleFocus={this.handleOnFocus}
|
||||
@@ -685,7 +679,6 @@ class RegistrationPage extends React.Component {
|
||||
(value) => this.setState(prevState => ({ values: { ...prevState.values, country: value } }))
|
||||
}
|
||||
errorCode={this.state.errorCode}
|
||||
readOnly={this.state.readOnly}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
@@ -824,16 +817,12 @@ class RegistrationPage extends React.Component {
|
||||
<CountryDropdown
|
||||
name="country"
|
||||
floatingLabel={intl.formatMessage(messages['registration.country.label'])}
|
||||
options={getCountryList(getLocale())}
|
||||
valueKey="code"
|
||||
displayValueKey="name"
|
||||
options={this.countryList}
|
||||
value={this.state.country}
|
||||
handleBlur={this.handleOnBlur}
|
||||
handleFocus={this.handleOnFocus}
|
||||
errorMessage={intl.formatMessage(messages['empty.country.field.error'])}
|
||||
handleChange={(value) => this.handleCountryChange(value)}
|
||||
errorMessage={this.state.errors.country}
|
||||
errorCode={this.state.errorCode}
|
||||
readOnly={this.state.readOnly}
|
||||
/>
|
||||
)}
|
||||
{formFields}
|
||||
|
||||
@@ -169,3 +169,6 @@ export const DEFAULT_TOP_LEVEL_DOMAINS = [
|
||||
'xyz', 'yachts', 'yahoo', 'yamaxun', 'yandex', 'ye', 'yodobashi', 'yoga', 'yokohama', 'you', 'youtube', 'yt',
|
||||
'yun', 'za', 'zappos', 'zara', 'zero', 'zip', 'zippo', 'zm', 'zone', 'zuerich', 'zw',
|
||||
];
|
||||
|
||||
export const COUNTRY_CODE_KEY = 'code';
|
||||
export const COUNTRY_DISPLAY_KEY = 'name';
|
||||
|
||||
@@ -350,7 +350,7 @@ describe('RegistrationPage', () => {
|
||||
expect(registrationPage.find('div[feedback-for="password"]').text()).toContain(emptyFieldValidation.password);
|
||||
registrationPage.find('input#password').simulate('focus');
|
||||
expect(registrationPage.find('div[feedback-for="country"]').text()).toEqual(emptyFieldValidation.country);
|
||||
registrationPage.find('input#country').simulate('blur', { target: { value: 'US', name: 'country' } });
|
||||
registrationPage.find('input#country').simulate('focus');
|
||||
expect(registrationPage.find('RegistrationPage').state('errors')).toEqual(errors);
|
||||
});
|
||||
|
||||
@@ -363,12 +363,6 @@ describe('RegistrationPage', () => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith(clearUsernameSuggestions());
|
||||
});
|
||||
|
||||
it('should set readOnly state false if focus on country field', () => {
|
||||
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
registrationPage.find('input#country').simulate('focus');
|
||||
expect(registrationPage.find('RegistrationPage').state('readOnly')).toEqual(false);
|
||||
});
|
||||
|
||||
// ******** test alert messages ********
|
||||
|
||||
it('should match third party auth alert', () => {
|
||||
@@ -958,11 +952,40 @@ describe('RegistrationPage', () => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith(setRegistrationFormData(formData));
|
||||
});
|
||||
|
||||
it('should set country in redux store on country change', () => {
|
||||
it('should set country code in redux store on country field blur', () => {
|
||||
const formData = {
|
||||
country: 'PK',
|
||||
errors: {
|
||||
country: '',
|
||||
email: '',
|
||||
name: '',
|
||||
password: '',
|
||||
username: '',
|
||||
},
|
||||
};
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const registerPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
registerPage.find('input#country').simulate('change', { target: { value: 'Pakistan', name: 'country' } });
|
||||
expect(store.dispatch).toHaveBeenCalledWith(setRegistrationFormData({ country: 'PK' }));
|
||||
registerPage.find('RegistrationPage').setState({ country: 'PK' });
|
||||
registerPage.find('input#country').simulate('blur');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(setRegistrationFormData(formData));
|
||||
});
|
||||
|
||||
it('should set country value with field error in redux store on country field blur', () => {
|
||||
const formData = {
|
||||
country: 'test',
|
||||
errors: {
|
||||
country: 'Select your country or region of residence',
|
||||
email: '',
|
||||
name: '',
|
||||
password: '',
|
||||
username: '',
|
||||
},
|
||||
};
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const registerPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
registerPage.find('RegistrationPage').setState({ country: 'test' });
|
||||
registerPage.find('input#country').simulate('blur');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(setRegistrationFormData(formData));
|
||||
});
|
||||
|
||||
it('should set country in component state on country change', () => {
|
||||
@@ -1042,6 +1065,7 @@ describe('RegistrationPage', () => {
|
||||
const registerPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
|
||||
populateRequiredFields(registerPage, payload);
|
||||
registerPage.find('RegistrationPage').setState({ values: { country: 'PK' } });
|
||||
registerPage.find('input#profession').simulate('change', { target: { value: 'Engineer', name: 'profession' } });
|
||||
registerPage.find('button.btn-brand').simulate('click');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
|
||||
|
||||
Reference in New Issue
Block a user