Add searchable dropdown
Add searchable dropdown field to make the UI complaint with the suggestions. VAN-498
This commit is contained in:
5
package-lock.json
generated
5
package-lock.json
generated
@@ -20967,6 +20967,11 @@
|
||||
"@emotion/core": "^10.0.22"
|
||||
}
|
||||
},
|
||||
"react-onclickoutside": {
|
||||
"version": "6.11.2",
|
||||
"resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.11.2.tgz",
|
||||
"integrity": "sha512-640486eSwU/t5iD6yeTlefma8dI3bxPXD93hM9JGKyYITAd0P1JFkkcDeyHZRqNpY/fv1YW0Fad9BXr44OY8wQ=="
|
||||
},
|
||||
"react-overlays": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-4.1.1.tgz",
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
"react-dom": "16.14.0",
|
||||
"react-helmet": "6.1.0",
|
||||
"react-loading-skeleton": "2.2.0",
|
||||
"react-onclickoutside": "^6.11.2",
|
||||
"react-redux": "7.2.3",
|
||||
"react-responsive": "8.2.0",
|
||||
"react-router": "5.2.0",
|
||||
|
||||
@@ -425,13 +425,11 @@ select.form-control {
|
||||
.medium-screen-svg {
|
||||
fill: $primary;
|
||||
overflow: inherit;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.large-screen-svg {
|
||||
fill: $primary;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.small-screen-header {
|
||||
@@ -518,7 +516,7 @@ select.form-control {
|
||||
}
|
||||
|
||||
.large-heading {
|
||||
margin-left: 7px;
|
||||
margin-left: 8px;
|
||||
color: $white;
|
||||
max-width: 24rem;
|
||||
line-height: 78px;
|
||||
@@ -541,9 +539,9 @@ select.form-control {
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 4.44rem;
|
||||
margin-top: 1.5rem;
|
||||
margin-left: 1.5rem;
|
||||
width: 4.5rem;
|
||||
padding-top: 2rem;
|
||||
padding-left: 25px;
|
||||
}
|
||||
|
||||
.username-suggestion {
|
||||
@@ -612,3 +610,33 @@ select.form-control {
|
||||
color: $gray-700;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.dropdown-item:active {
|
||||
background-color: #F2F0EF;
|
||||
}
|
||||
|
||||
.dropdown-container {
|
||||
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.15), 0px 2px 8px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 4px;
|
||||
max-height: 200px;
|
||||
font-size: 1rem;
|
||||
font-weight: normal;
|
||||
line-height: 1.25rem;
|
||||
overflow-y: scroll;
|
||||
position: absolute;
|
||||
background-color: #fff;
|
||||
width: 464px;
|
||||
}
|
||||
|
||||
@media (max-width: 464px) {
|
||||
.dropdown-container {
|
||||
width: auto;
|
||||
left: 0;
|
||||
right: 0;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.-mt-4 {
|
||||
margin-top: -1.5rem;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ const FormGroup = (props) => {
|
||||
setHasFocus(true);
|
||||
if (props.handleFocus) { props.handleFocus(e); }
|
||||
};
|
||||
const handleClick = (e) => {
|
||||
if (props.handleClick) { props.handleClick(e); }
|
||||
};
|
||||
const handleOnBlur = (e) => {
|
||||
setHasFocus(false);
|
||||
if (props.handleBlur) { props.handleBlur(e); }
|
||||
@@ -24,10 +27,12 @@ const FormGroup = (props) => {
|
||||
as={props.as}
|
||||
type={props.type}
|
||||
|
||||
autoComplete={props.autoComplete}
|
||||
name={props.name}
|
||||
value={props.value}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleOnBlur}
|
||||
onClick={handleClick}
|
||||
onChange={props.handleChange}
|
||||
controlClassName={props.borderClass}
|
||||
|
||||
@@ -66,9 +71,11 @@ FormGroup.defaultProps = {
|
||||
borderClass: '',
|
||||
suggestedTopLevelDomain: '',
|
||||
suggestedServiceLevelDomain: '',
|
||||
autoComplete: null,
|
||||
handleBlur: null,
|
||||
handleChange: () => {},
|
||||
handleFocus: null,
|
||||
handleClick: null,
|
||||
helpText: [],
|
||||
options: null,
|
||||
trailingElement: null,
|
||||
@@ -82,10 +89,12 @@ FormGroup.propTypes = {
|
||||
borderClass: PropTypes.string,
|
||||
suggestedTopLevelDomain: PropTypes.string,
|
||||
suggestedServiceLevelDomain: PropTypes.string,
|
||||
autoComplete: PropTypes.string,
|
||||
floatingLabel: PropTypes.string.isRequired,
|
||||
handleBlur: PropTypes.func,
|
||||
handleChange: PropTypes.func,
|
||||
handleFocus: PropTypes.func,
|
||||
handleClick: PropTypes.func,
|
||||
helpText: PropTypes.arrayOf(PropTypes.string),
|
||||
name: PropTypes.string.isRequired,
|
||||
options: PropTypes.func,
|
||||
|
||||
172
src/register/CountryDropdown.jsx
Normal file
172
src/register/CountryDropdown.jsx
Normal file
@@ -0,0 +1,172 @@
|
||||
import React from 'react';
|
||||
import { Icon } from '@edx/paragon';
|
||||
import { ExpandMore, ExpandLess } from '@edx/paragon/icons';
|
||||
import onClickOutside from 'react-onclickoutside';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormGroup } from '../common-components';
|
||||
import { FORM_SUBMISSION_ERROR } from './data/constants';
|
||||
|
||||
class CountryDropdown extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
displayValue: '',
|
||||
icon: ExpandMore,
|
||||
errorMessage: '',
|
||||
showFieldError: true,
|
||||
};
|
||||
|
||||
this.handleFocus = this.handleFocus.bind(this);
|
||||
this.handleOnBlur = this.handleOnBlur.bind(this);
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
if (props.errorCode === FORM_SUBMISSION_ERROR && state.showFieldError) {
|
||||
return { errorMessage: props.errorMessage };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getItems(strToFind = '') {
|
||||
let { options } = this.props;
|
||||
if (strToFind.length > 0) {
|
||||
options = options.filter((option) => (option.name.toLowerCase().includes(strToFind.toLowerCase())));
|
||||
}
|
||||
|
||||
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('...');
|
||||
}
|
||||
|
||||
return (
|
||||
<button type="button" className="dropdown-item" value={value} key={value} onClick={(e) => { this.handleItemClick(e); }}>
|
||||
{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] });
|
||||
}
|
||||
}
|
||||
|
||||
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] });
|
||||
} else {
|
||||
this.setValue(null);
|
||||
this.setState({ displayValue: value });
|
||||
}
|
||||
}
|
||||
|
||||
handleClick = () => {
|
||||
if (!this.props.value) {
|
||||
const dropDownItems = this.getItems();
|
||||
this.setState({
|
||||
dropDownItems, icon: ExpandLess, errorMessage: '', showFieldError: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleOnChange = (e) => {
|
||||
const findstr = e.target.value;
|
||||
|
||||
if (findstr.length > 0) {
|
||||
const filteredItems = this.getItems(findstr);
|
||||
this.setState({ dropDownItems: filteredItems, icon: ExpandLess, errorMessage: '' });
|
||||
} else {
|
||||
this.setState({ dropDownItems: '', icon: ExpandMore, errorMessage: this.props.errorMessage });
|
||||
}
|
||||
|
||||
this.setDisplayValue(e.target.value);
|
||||
}
|
||||
|
||||
handleClickOutside = () => {
|
||||
if (this.state.dropDownItems?.length > 0) {
|
||||
const msg = this.state.displayValue === '' ? this.props.errorMessage : '';
|
||||
this.setState(() => ({
|
||||
icon: ExpandMore,
|
||||
dropDownItems: '',
|
||||
errorMessage: msg,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
handleFocus(e) {
|
||||
if (this.props.handleFocus) { this.props.handleFocus(e); }
|
||||
}
|
||||
|
||||
handleOnBlur(e) {
|
||||
if (this.props.handleBlur) { this.props.handleBlur(e); }
|
||||
}
|
||||
|
||||
handleItemClick(e) {
|
||||
this.setValue(e.target.value);
|
||||
this.setState({ dropDownItems: '', icon: ExpandMore });
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<FormGroup
|
||||
as="input"
|
||||
name={this.props.name}
|
||||
autoComplete="off"
|
||||
floatingLabel={this.props.floatingLabel}
|
||||
trailingElement={<Icon src={this.state.icon} />}
|
||||
handleChange={this.handleOnChange}
|
||||
handleClick={this.handleClick}
|
||||
handleBlur={this.handleOnBlur}
|
||||
handleFocus={this.handleOnFocus}
|
||||
value={this.state.displayValue}
|
||||
errorMessage={this.state.errorMessage}
|
||||
/>
|
||||
<div className="dropdown-container -mt-4">
|
||||
{ this.state.dropDownItems?.length > 0 ? this.state.dropDownItems : null }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CountryDropdown.defaultProps = {
|
||||
options: null,
|
||||
floatingLabel: null,
|
||||
handleFocus: null,
|
||||
handleChange: null,
|
||||
handleBlur: null,
|
||||
value: null,
|
||||
errorMessage: null,
|
||||
};
|
||||
|
||||
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.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default onClickOutside(CountryDropdown);
|
||||
@@ -13,9 +13,8 @@ import {
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||
import {
|
||||
Form, Hyperlink, Icon, StatefulButton,
|
||||
Form, Hyperlink, StatefulButton,
|
||||
} from '@edx/paragon';
|
||||
import { ExpandMore } from '@edx/paragon/icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { closest } from 'fastest-levenshtein';
|
||||
|
||||
@@ -45,6 +44,7 @@ import {
|
||||
import {
|
||||
getTpaProvider, getTpaHint, getAllPossibleQueryParam, setSurveyCookie,
|
||||
} from '../data/utils';
|
||||
import CountryDropdown from './CountryDropdown';
|
||||
|
||||
class RegistrationPage extends React.Component {
|
||||
constructor(props, context) {
|
||||
@@ -131,12 +131,6 @@ class RegistrationPage extends React.Component {
|
||||
}
|
||||
};
|
||||
|
||||
getCountryOptions = () => [
|
||||
{ code: '', name: this.props.intl.formatMessage(messages['registration.country.label']) },
|
||||
].concat(getCountryList(getLocale())).map(({ code, name }) => (
|
||||
<option className="data-hj-suppress" key={code} value={code}>{name}</option>
|
||||
));
|
||||
|
||||
getOptionalFields() {
|
||||
return (
|
||||
<OptionalFields
|
||||
@@ -498,19 +492,20 @@ class RegistrationPage extends React.Component {
|
||||
floatingLabel={intl.formatMessage(messages['registration.password.label'])}
|
||||
/>
|
||||
)}
|
||||
<FormGroup
|
||||
as="select"
|
||||
<CountryDropdown
|
||||
name="country"
|
||||
floatingLabel={intl.formatMessage(messages['registration.country.label'])}
|
||||
options={getCountryList(getLocale())}
|
||||
valueKey="code"
|
||||
displayValueKey="name"
|
||||
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} />}
|
||||
options={this.getCountryOptions}
|
||||
errorMessage={intl.formatMessage(messages['empty.country.field.error'])}
|
||||
handleChange={(value) => this.setState({ country: value })}
|
||||
errorCode={this.state.errorCode}
|
||||
/>
|
||||
<div id="honor-code" className="small">
|
||||
<div id="honor-code" className="small mt-4">
|
||||
<FormattedMessage
|
||||
id="register.page.terms.of.service.and.honor.code"
|
||||
defaultMessage="By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each
|
||||
|
||||
@@ -89,7 +89,7 @@ describe('RegistrationPage', () => {
|
||||
registerPage.find('input#name').simulate('change', { target: { value: payload.name, name: 'name' } });
|
||||
registerPage.find('input#username').simulate('change', { target: { value: payload.username, name: 'username' } });
|
||||
registerPage.find('input#email').simulate('change', { target: { value: payload.email, name: 'email' } });
|
||||
registerPage.find('select#country').simulate('change', { target: { value: payload.country, name: 'country' } });
|
||||
registerPage.find('input#country').simulate('change', { target: { value: payload.country } });
|
||||
|
||||
if (!isThirdPartyAuth) {
|
||||
registerPage.find('input#password').simulate('change', { target: { value: payload.password, name: 'password' } });
|
||||
@@ -138,7 +138,7 @@ describe('RegistrationPage', () => {
|
||||
|
||||
populateRequiredFields(registerPage, payload);
|
||||
registerPage.find('button.btn-brand').simulate('click');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser(payload));
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
|
||||
});
|
||||
|
||||
it('should submit form without password field when current provider is present', () => {
|
||||
@@ -169,7 +169,7 @@ describe('RegistrationPage', () => {
|
||||
|
||||
populateRequiredFields(registerPage, formPayload, true);
|
||||
registerPage.find('button.btn-brand').simulate('click');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser(formPayload));
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...formPayload, country: 'PK' }));
|
||||
});
|
||||
|
||||
it('should not dispatch registerNewUser on empty form Submission', () => {
|
||||
@@ -219,7 +219,7 @@ describe('RegistrationPage', () => {
|
||||
registrationPage.find('input#name').simulate('blur', { target: { value: '', name: 'name' } });
|
||||
registrationPage.find('input#email').simulate('blur', { target: { value: '', name: 'email' } });
|
||||
registrationPage.find('input#password').simulate('blur', { target: { value: '', name: 'password' } });
|
||||
registrationPage.find('select#country').simulate('blur', { target: { value: '', name: 'country' } });
|
||||
registrationPage.find('input#country').simulate('blur', { target: { value: '', name: 'country' } });
|
||||
expect(registrationPage.find('RegistrationPage').state('errors')).toEqual(emptyFieldValidation);
|
||||
});
|
||||
|
||||
@@ -315,7 +315,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('select#country').simulate('blur', { target: { value: 'US', name: 'country' } });
|
||||
registrationPage.find('input#country').simulate('blur', { target: { value: 'US', name: 'country' } });
|
||||
expect(registrationPage.find('RegistrationPage').state('errors')).toEqual(errors);
|
||||
});
|
||||
|
||||
@@ -784,7 +784,7 @@ describe('RegistrationPage', () => {
|
||||
registerPage.find('textarea#goals').simulate('change', { target: { value: 'edX goals', name: 'goals' } });
|
||||
|
||||
registerPage.find('button.btn-brand').simulate('click');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser(payload));
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user