Skip to content

Commit

Permalink
refactor: registration component refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
syedsajjadkazmii committed Aug 29, 2023
1 parent c5caaeb commit 8d6473f
Show file tree
Hide file tree
Showing 30 changed files with 1,096 additions and 1,049 deletions.
56 changes: 54 additions & 2 deletions src/common-components/PasswordField.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';

import { useIntl } from '@edx/frontend-platform/i18n';
import {
Expand All @@ -11,31 +12,80 @@ import PropTypes from 'prop-types';

import messages from './messages';
import { LETTER_REGEX, NUMBER_REGEX } from '../data/constants';
import { isHostAvailableInQueryParams } from '../data/utils';
import { clearRegistertionBackendError, fetchRealtimeValidations } from '../register/data/actions';
import { PASSWORD_FIELD_LABEL } from '../register/data/constants';
import { validatePasswordField } from '../register/data/utils';

const PasswordField = (props) => {
const { formatMessage } = useIntl();
const dispatch = useDispatch();

const validationApiRateLimited = useSelector(state => state.register.validationApiRateLimited);
const [isPasswordHidden, setHiddenTrue, setHiddenFalse] = useToggle(true);
const [showTooltip, setShowTooltip] = useState(false);
const registrationEmbedded = isHostAvailableInQueryParams();

const handleBlur = (e) => {
if (e.target?.name === PASSWORD_FIELD_LABEL && e.relatedTarget?.name === 'passwordIcon') {
return; // resolving a bug where validations get run on password icon focus
}

if (props.handleBlur) { props.handleBlur(e); }
setShowTooltip(props.showRequirements && false);
if (props.handleErrorChange) { // If rendering from register page
const fieldError = validatePasswordField(props.value, formatMessage);
if (fieldError) {
props.handleErrorChange(PASSWORD_FIELD_LABEL, fieldError);
} else if (!registrationEmbedded && !validationApiRateLimited) {
dispatch(fetchRealtimeValidations({ [PASSWORD_FIELD_LABEL]: props.value }));
}
}
};

const handleFocus = (e) => {
if (e.target?.name === 'passwordIcon') {
return; // resolving a bug where error gets cleared on password icon focus
}

if (props.handleFocus) {
props.handleFocus(e);
}
if (props.handleErrorChange) {
props.handleErrorChange(PASSWORD_FIELD_LABEL, '');
dispatch(clearRegistertionBackendError(PASSWORD_FIELD_LABEL));
}
setTimeout(() => setShowTooltip(props.showRequirements && true), 150);
};

const HideButton = (
<IconButton onFocus={handleFocus} onBlur={handleBlur} name="password" src={VisibilityOff} iconAs={Icon} onClick={setHiddenTrue} size="sm" variant="secondary" alt={formatMessage(messages['hide.password'])} />
<IconButton
onFocus={handleFocus}
onBlur={handleBlur}
name="passwordIcon"
src={VisibilityOff}
iconAs={Icon}
onClick={setHiddenTrue}
size="sm"
variant="secondary"
alt={formatMessage(messages['hide.password'])}
/>
);

const ShowButton = (
<IconButton onFocus={handleFocus} onBlur={handleBlur} name="password" src={Visibility} iconAs={Icon} onClick={setHiddenFalse} size="sm" variant="secondary" alt={formatMessage(messages['show.password'])} />
<IconButton
onFocus={handleFocus}
onBlur={handleBlur}
name="passwordIcon"
src={Visibility}
iconAs={Icon}
onClick={setHiddenFalse}
size="sm"
variant="secondary"
alt={formatMessage(messages['show.password'])}
/>
);

const placement = window.innerWidth < 768 ? 'top' : 'left';
const tooltip = (
<Tooltip id={`password-requirement-${placement}`}>
Expand Down Expand Up @@ -89,6 +139,7 @@ PasswordField.defaultProps = {
handleBlur: null,
handleFocus: null,
handleChange: () => {},
handleErrorChange: null,
showRequirements: true,
autoComplete: null,
};
Expand All @@ -100,6 +151,7 @@ PasswordField.propTypes = {
handleBlur: PropTypes.func,
handleFocus: PropTypes.func,
handleChange: PropTypes.func,
handleErrorChange: PropTypes.func,
name: PropTypes.string.isRequired,
showRequirements: PropTypes.bool,
value: PropTypes.string.isRequired,
Expand Down
6 changes: 0 additions & 6 deletions src/data/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,8 @@ export const FAILURE_STATE = 'failure';
export const FORBIDDEN_STATE = 'forbidden';
export const EMBEDDED = 'embedded';

// Regex
export const VALID_EMAIL_REGEX = '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+)*'
+ '|^"([\\001-\\010\\013\\014\\016-\\037!#-\\[\\]-\\177]|\\\\[\\001-\\011\\013\\014\\016-\\177])*"'
+ ')@((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\\.)+)(?:[A-Z0-9-]{2,63})'
+ '|\\[(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\]$';
export const LETTER_REGEX = /[a-zA-Z]/;
export const NUMBER_REGEX = /\d/;
export const INVALID_NAME_REGEX = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?/gi; // eslint-disable-line no-useless-escape

// Query string parameters that can be passed to LMS to manage
// things like auto-enrollment upon login and registration.
Expand Down
3 changes: 2 additions & 1 deletion src/forgot-password/ForgotPasswordPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ import ForgotPasswordAlert from './ForgotPasswordAlert';
import messages from './messages';
import BaseContainer from '../base-container';
import { FormGroup } from '../common-components';
import { DEFAULT_STATE, LOGIN_PAGE, VALID_EMAIL_REGEX } from '../data/constants';
import { DEFAULT_STATE, LOGIN_PAGE } from '../data/constants';
import { updatePathWithQueryParams, windowScrollTo } from '../data/utils';
import { VALID_EMAIL_REGEX } from '../register/RegistrationFields/EmailField/constants';

const ForgotPasswordPage = (props) => {
const platformName = getConfig().SITE_NAME;
Expand Down
134 changes: 134 additions & 0 deletions src/register/RegistrationFields/CountryField/CountryField.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';

import { useIntl } from '@edx/frontend-platform/i18n';
import { FormAutosuggest, FormAutosuggestOption, FormControlFeedback } from '@edx/paragon';
import PropTypes from 'prop-types';

import { COUNTRY_CODE_KEY, COUNTRY_DISPLAY_KEY, COUNTRY_FIELD_LABEL } from './constants';
import validateCountryField from './validator';
import { clearRegistertionBackendError } from '../../data/actions';
import messages from '../../messages';

const CountryField = (props) => {
const {
countryList,
selectedCountry,
onChangeHandler,
handleErrorChange,
onFocusHandler,
} = props;
const { formatMessage } = useIntl();
const dispatch = useDispatch();
const backendCountryCode = useSelector(state => state.register.backendCountryCode);

useEffect(() => {
if (backendCountryCode && backendCountryCode !== selectedCountry?.countryCode) {
let countryCode = '';
let countryDisplayValue = '';

const countryVal = countryList.find(
(country) => (country[COUNTRY_CODE_KEY].toLowerCase() === backendCountryCode.toLowerCase()),
);
if (countryVal) {
countryCode = countryVal[COUNTRY_CODE_KEY];
countryDisplayValue = countryVal[COUNTRY_DISPLAY_KEY];
}
onChangeHandler(
{ target: { name: COUNTRY_FIELD_LABEL } },
{ countryCode, displayValue: countryDisplayValue },
);
}
}, [backendCountryCode, countryList]); // eslint-disable-line react-hooks/exhaustive-deps

const handleOnBlur = (event) => {
// Do not run validations when drop-down arrow is clicked
if (event.relatedTarget && event.relatedTarget.className.includes('pgn__form-autosuggest__icon-button')) {
return;
}

const { value } = event.target;

const { countryCode, displayValue, error } = validateCountryField(
value.trim(), countryList, formatMessage(messages['empty.country.field.error']),
);

onChangeHandler({ target: { name: COUNTRY_FIELD_LABEL } }, { countryCode, displayValue });
handleErrorChange(COUNTRY_FIELD_LABEL, error);
// onBlurHandler(event);
};

const handleSelected = (value) => {
handleOnBlur({ target: { name: COUNTRY_FIELD_LABEL, value } });
};

const handleOnFocus = (event) => {
handleErrorChange(COUNTRY_FIELD_LABEL, '');
dispatch(clearRegistertionBackendError(COUNTRY_FIELD_LABEL));
onFocusHandler(event);
};

const handleOnChange = (value) => {
onChangeHandler({ target: { name: COUNTRY_FIELD_LABEL } }, { countryCode: '', displayValue: value });
};

const getCountryList = () => countryList.map((country) => (
<FormAutosuggestOption key={country[COUNTRY_CODE_KEY]}>
{country[COUNTRY_DISPLAY_KEY]}
</FormAutosuggestOption>
));

return (
<div className="mb-4">
<FormAutosuggest
floatingLabel={formatMessage(messages['registration.country.label'])}
aria-label="form autosuggest"
name="country"
value={selectedCountry.displayValue || ''}
onSelected={(value) => handleSelected(value)}
onFocus={(e) => handleOnFocus(e)}
onBlur={(e) => handleOnBlur(e)}
onChange={(value) => handleOnChange(value)}
>
{getCountryList()}
</FormAutosuggest>
{props.errorMessage !== '' && (
<FormControlFeedback
key="error"
className="form-text-size"
hasIcon={false}
feedback-for="country"
type="invalid"
>
{props.errorMessage}
</FormControlFeedback>
)}
</div>
);
};

CountryField.propTypes = {
countryList: PropTypes.arrayOf(
PropTypes.shape({
code: PropTypes.string,
name: PropTypes.string,
}),
).isRequired,
errorMessage: PropTypes.string,
onChangeHandler: PropTypes.func.isRequired,
handleErrorChange: PropTypes.func.isRequired,
onFocusHandler: PropTypes.func.isRequired,
selectedCountry: PropTypes.shape({
displayValue: PropTypes.string,
countryCode: PropTypes.string,
}),
};

CountryField.defaultProps = {
errorMessage: null,
selectedCountry: {
value: '',
},
};

export default CountryField;
3 changes: 3 additions & 0 deletions src/register/RegistrationFields/CountryField/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const COUNTRY_FIELD_LABEL = 'country';
export const COUNTRY_CODE_KEY = 'code';
export const COUNTRY_DISPLAY_KEY = 'name';
28 changes: 28 additions & 0 deletions src/register/RegistrationFields/CountryField/validator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { COUNTRY_CODE_KEY, COUNTRY_DISPLAY_KEY } from './constants';

const validateCountryField = (value, countryList, errorMessage) => {
let countryCode = '';
let displayValue = value;
let error = errorMessage;

if (value) {
const normalizedValue = value.toLowerCase();
// Handling a case here where user enters a valid country code that needs to be
// evaluated and set its value as a valid value.
const selectedCountry = countryList.find(
(country) => (
// When translations are applied, extra space added in country value, so we should trim that.
country[COUNTRY_DISPLAY_KEY].toLowerCase().trim() === normalizedValue
|| country[COUNTRY_CODE_KEY].toLowerCase().trim() === normalizedValue
),
);
if (selectedCountry) {
countryCode = selectedCountry[COUNTRY_CODE_KEY];
displayValue = selectedCountry[COUNTRY_DISPLAY_KEY];
error = '';
}
}
return { error, countryCode, displayValue };
};

export default validateCountryField;
Loading

0 comments on commit 8d6473f

Please sign in to comment.