diff --git a/src/register/ConfigurableRegistrationForm.jsx b/src/register/ConfigurableRegistrationForm.jsx index 36bc66a9..844949f0 100644 --- a/src/register/ConfigurableRegistrationForm.jsx +++ b/src/register/ConfigurableRegistrationForm.jsx @@ -54,13 +54,13 @@ const ConfigurableRegistrationForm = (props) => { }); const handleOnChange = (event, countryValue = null) => { - const { name } = event.target; + const { name, type } = event.target; let value; if (countryValue) { value = { ...countryValue }; } else { value = event.target.type === 'checkbox' ? event.target.checked : event.target.value; - if (event.target.type === 'checkbox') { + if (type === 'checkbox') { setFieldErrors(prevErrors => ({ ...prevErrors, [name]: '' })); } } diff --git a/src/register/data/constants.js b/src/register/data/constants.js index 34d75e4b..0c0f68fb 100644 --- a/src/register/data/constants.js +++ b/src/register/data/constants.js @@ -173,3 +173,6 @@ export const DEFAULT_TOP_LEVEL_DOMAINS = [ export const COUNTRY_CODE_KEY = 'code'; export const COUNTRY_DISPLAY_KEY = 'name'; + +export const EXPAND_MORE_ICON = 'expand-more'; +export const EXPAND_LESS_ICON = 'expand-less'; diff --git a/src/register/data/utils.js b/src/register/data/utils.js index f34a0aed..99f3ba0c 100644 --- a/src/register/data/utils.js +++ b/src/register/data/utils.js @@ -93,10 +93,14 @@ export function validateCountryField(value, countryList, 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. + // When translations apply 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) { diff --git a/src/register/registrationFields/CountryField.jsx b/src/register/registrationFields/CountryField.jsx index 0465bfac..cbe8440e 100644 --- a/src/register/registrationFields/CountryField.jsx +++ b/src/register/registrationFields/CountryField.jsx @@ -1,62 +1,154 @@ -import React from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { useIntl } from '@edx/frontend-platform/i18n'; -import { FormAutosuggest, FormAutosuggestOption, FormControlFeedback } from '@edx/paragon'; +import { Icon, IconButton } from '@edx/paragon'; +import { ExpandLess, ExpandMore } from '@edx/paragon/icons'; import PropTypes from 'prop-types'; -import { COUNTRY_CODE_KEY, COUNTRY_DISPLAY_KEY } from '../data/constants'; +import { FormGroup } from '../../common-components'; +import { + COUNTRY_CODE_KEY, COUNTRY_DISPLAY_KEY, EXPAND_LESS_ICON, EXPAND_MORE_ICON, +} from '../data/constants'; import messages from '../messages'; const CountryField = (props) => { const { countryList, selectedCountry } = props; - const { formatMessage } = useIntl(); - const handleSelected = (value) => { - if (props.onBlurHandler) { props.onBlurHandler({ target: { name: 'country', value } }); } + const dropdownRef = useRef(null); + const { formatMessage } = useIntl(); + const [errorMessage, setErrorMessage] = useState(props.errorMessage); + const [dropDownItems, setDropDownItems] = useState([]); + const [displayValue, setDisplayValue] = useState(''); + const [trailingIcon, setTrailingIcon] = useState(EXPAND_MORE_ICON); + + const onBlurHandler = (event, itemClicked = false, countryName = '') => { + const { name } = event.target; + const relatedName = event.relatedTarget ? event.relatedTarget.name : ''; + // For a better user experience, do not validate when focus out from 'country' field + // and focus on 'countryItem' or 'countryExpand' button. + if ((relatedName === 'countryItem' || relatedName === 'countryExpand') && name === 'country') { + return; + } + const countryValue = itemClicked ? countryName : displayValue; + if (props.onBlurHandler) { + props.onBlurHandler({ target: { name: 'country', value: countryValue } }); + } + setTrailingIcon(EXPAND_MORE_ICON); + setDropDownItems([]); + }; + + const getDropdownItems = (countryToFind = null) => { + let updatedCountryList = countryList; + if (countryToFind) { + updatedCountryList = countryList.filter( + (option) => (option.name.toLowerCase().includes(countryToFind.toLowerCase())), + ); + } + + return updatedCountryList.map((country) => { + const countryName = country[COUNTRY_DISPLAY_KEY]; + return ( + + ); + }); }; const onFocusHandler = (event) => { + const { name, value } = event.target; + setDropDownItems(getDropdownItems(name === 'country' ? value : displayValue)); + setTrailingIcon(EXPAND_LESS_ICON); + setErrorMessage(''); if (props.onFocusHandler) { props.onFocusHandler(event); } }; - const onChangeHandler = (value) => { - if (props.onChangeHandler) { - props.onChangeHandler({ target: { name: 'country' } }, { countryCode: '', displayValue: value }); + const onChangeHandler = (event) => { + const filteredItems = getDropdownItems(event.target.value); + setDropDownItems(filteredItems); + setDisplayValue(event.target.value); + if (props.onChangeHandler) { props.onChangeHandler(event, { countryCode: '', displayValue: event.target.value }); } + }; + + const handleOnClickOutside = () => { + setTrailingIcon(EXPAND_MORE_ICON); + setDropDownItems([]); + }; + + const handleTrailingIconClick = () => { + if (trailingIcon === EXPAND_MORE_ICON) { + setDropDownItems(getDropdownItems()); + setTrailingIcon(EXPAND_LESS_ICON); + } else { + setDropDownItems([]); + setTrailingIcon(EXPAND_MORE_ICON); } }; - const getCountryList = () => countryList.map((country) => ( - - {country[COUNTRY_DISPLAY_KEY]} - - )); + useEffect(() => { + const handleClickOutside = (event) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { + handleOnClickOutside(); + } + }; + document.addEventListener('click', handleClickOutside, true); + return () => { + document.removeEventListener('click', handleClickOutside, true); + }; + }, []); + + useEffect(() => { + if (selectedCountry.displayValue) { + setDisplayValue(selectedCountry.displayValue); + } + }, [selectedCountry]); + + useEffect(() => { + setErrorMessage(props.errorMessage); + }, [props.errorMessage]); return ( - <> - + handleSelected(value)} - onFocus={(e) => onFocusHandler(e)} - onBlur={(e) => handleSelected(e.target.value)} - onChange={(value) => onChangeHandler(value)} - > - {getCountryList()} - - {props.errorMessage !== '' && ( - - {props.errorMessage} - - )} - + autoComplete="chrome-off" + className="mb-0" + floatingLabel={formatMessage(messages['registration.country.label'])} + trailingElement={( + {}} + onClick={handleTrailingIconClick} + onFocus={() => {}} + /> + )} + value={displayValue} + errorMessage={errorMessage} + handleChange={onChangeHandler} + handleBlur={onBlurHandler} + handleFocus={onFocusHandler} + /> +
+ { dropDownItems?.length > 0 ? dropDownItems : null } +
+ ); }; diff --git a/src/register/tests/RegistrationPage.test.jsx b/src/register/tests/RegistrationPage.test.jsx index d4ebb0f2..2a580eb1 100644 --- a/src/register/tests/RegistrationPage.test.jsx +++ b/src/register/tests/RegistrationPage.test.jsx @@ -121,8 +121,8 @@ describe('RegistrationPage', () => { registrationPage.find('input#username').simulate('change', { target: { value: payload.username, name: 'username' } }); registrationPage.find('input#email').simulate('change', { target: { value: payload.email, name: 'email' } }); - registrationPage.find('input[name="country"]').simulate('change', { target: { value: payload.country, name: 'country' } }); - registrationPage.find('input[name="country"]').simulate('blur', { target: { value: payload.country, name: 'country' } }); + registrationPage.find('input#country').simulate('change', { target: { value: payload.country, name: 'country' } }); + registrationPage.find('input#country').simulate('blur', { target: { value: payload.country, name: 'country' } }); if (!isThirdPartyAuth) { registrationPage.find('input#password').simulate('change', { target: { value: payload.password, name: 'password' } }); @@ -307,7 +307,7 @@ describe('RegistrationPage', () => { registrationPage.find('input#password').simulate('blur', { target: { value: '', name: 'password' } }); expect(registrationPage.find('div[feedback-for="password"]').text()).toContain(emptyFieldValidation.password); - registrationPage.find('input[name="country"]').simulate('blur', { target: { value: '', name: 'country' } }); + registrationPage.find('input#country').simulate('blur', { target: { value: '', name: 'country' } }); expect(registrationPage.find('div[feedback-for="country"]').text()).toEqual(emptyFieldValidation.country); }); @@ -332,7 +332,7 @@ describe('RegistrationPage', () => { it('should run validations for focused field on form submission', () => { const registrationPage = mount(reduxWrapper()); - registrationPage.find('input[name="country"]').simulate('focus'); + registrationPage.find('input#country').simulate('focus'); registrationPage.find('button.btn-brand').simulate('click'); expect(registrationPage.find('div[feedback-for="country"]').text()).toEqual(emptyFieldValidation.country); @@ -443,7 +443,7 @@ describe('RegistrationPage', () => { expect(registrationPage.find('div[feedback-for="password"]').exists()).toBeFalsy(); expect(registrationPage.find('div[feedback-for="country"]').text()).toEqual(emptyFieldValidation.country); - registrationPage.find('input[name="country"]').simulate('focus'); + registrationPage.find('input#country').simulate('focus'); expect(registrationPage.find('div[feedback-for="country"]').exists()).toBeFalsy(); }); @@ -986,11 +986,9 @@ describe('RegistrationPage', () => { backendCountryCode: 'PK', }, }); - const expectedPropValue = { countryCode: 'PK', displayValue: 'Pakistan' }; - const registrationPage = mount(reduxWrapper()).find('RegistrationPage'); - expect(registrationPage.find('CountryField').prop('selectedCountry')).toEqual(expectedPropValue); - expect(registrationPage.find('input[name="country"]').exists()).toBeTruthy(); + const registrationPage = mount(reduxWrapper()); + expect(registrationPage.find('input#country').props().value).toEqual('Pakistan'); }); it('should display error message based on the error code returned by API', () => { @@ -1043,7 +1041,7 @@ describe('RegistrationPage', () => { getLocale.mockImplementation(() => ('ar-ae')); const registrationPage = mount(reduxWrapper()); - registrationPage.find('input[name="country"]').simulate('click'); + registrationPage.find('input#country').simulate('focus'); registrationPage.find('button.dropdown-item').at(0).simulate('click', { target: { value: 'أفغانستان ', name: 'countryItem' } }); expect(registrationPage.find('div[feedback-for="country"]').exists()).toBeFalsy(); }); @@ -1066,6 +1064,18 @@ describe('RegistrationPage', () => { registrationPage.find('input#email').simulate('change', { target: { value: 'a@gmail.com', name: 'email' } }); expect(registrationPage.find('div[feedback-for="email"]').exists()).toBeFalsy(); }); + + it('should set country in component state when form is translated using browser translations', () => { + getLocale.mockImplementation(() => ('en-us')); + + store.dispatch = jest.fn(store.dispatch); + + const registrationPage = mount(reduxWrapper()); + registrationPage.find('input#country').simulate('focus'); + registrationPage.find('button.dropdown-item').at(0).simulate('click', { target: { value: undefined, name: undefined, parentElement: { parentElement: { value: 'Afghanistan' } } } }); + expect(registrationPage.find('input#country').props().value).toEqual('Afghanistan'); + expect(registrationPage.find('div[feedback-for="country"]').exists()).toBeFalsy(); + }); }); describe('Test Configurable Fields', () => { @@ -1094,7 +1104,6 @@ describe('RegistrationPage', () => { }); it('should submit form with fields returned by backend in payload', () => { - getLocale.mockImplementation(() => ('en-us')); jest.spyOn(global.Date, 'now').mockImplementation(() => 0); store = mockStore({ ...initialState, @@ -1195,6 +1204,16 @@ describe('RegistrationPage', () => { expect(registrationPage.find('#profession-error').last().text()).toEqual(professionError); }); + it('should not remove errors from form fields when country is selected by clicking on expand button', () => { + const registrationPage = mount(reduxWrapper()); + registrationPage.find('button.btn-brand').simulate('click'); + expect(registrationPage.find('div[feedback-for="name"]').exists()).toBeTruthy(); + + registrationPage.find('button[name="countryExpand"]').simulate('click'); + registrationPage.find('button.dropdown-item').at(0).simulate('click', { target: { value: 'Pakistan', name: 'countryItem' } }); + expect(registrationPage.find('div[feedback-for="name"]').exists()).toBeTruthy(); + }); + it('should check TOS and honor code fields if they exist when auto submitting register form', () => { getLocale.mockImplementation(() => ('en-us')); store = mockStore({ diff --git a/src/sass/_registration.scss b/src/sass/_registration.scss index 63baf2c8..5b4f93a0 100644 --- a/src/sass/_registration.scss +++ b/src/sass/_registration.scss @@ -1,7 +1,3 @@ .register-stateful-button-width { min-width: 14.4rem; } - -.pgn__form-autosuggest__wrapper > .pgn__form-group { - margin-bottom: 0 !important; -}