Files
frontend-app-authn/src/register/CountryDropdown.jsx

230 lines
7.0 KiB
JavaScript

import React from 'react';
import { Icon, IconButton } from '@edx/paragon';
import { ExpandLess, ExpandMore } from '@edx/paragon/icons';
import PropTypes from 'prop-types';
import onClickOutside from 'react-onclickoutside';
import { FormGroup } from '../common-components';
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,
};
}
shouldComponentUpdate(nextProps) {
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;
}
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[COUNTRY_CODE_KEY];
const displayValue = opt[COUNTRY_DISPLAY_KEY];
return (
<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>
);
});
}
handleOnChange = (e) => {
const filteredItems = this.getItems(e.target.value);
this.setState({
dropDownItems: filteredItems,
displayValue: e.target.value,
});
}
handleClickOutside = () => {
if (this.state.dropDownItems?.length > 0) {
this.setState(() => ({
icon: this.expandMoreButton(),
dropDownItems: '',
}));
}
}
handleExpandLess() {
this.setState({ dropDownItems: '', icon: this.expandMoreButton() });
}
handleExpandMore() {
this.setState(prevState => ({
dropDownItems: this.getItems(prevState.displayValue), icon: this.expandLessButton(), errorMessage: '', showFieldError: false,
}));
}
handleFocus(e) {
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, itemClicked = false, country = '') {
const { name } = e.target;
let countryValue = '';
if (country) {
countryValue = country;
} else {
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) {
let countryValue = '';
if (!e.target.value) {
countryValue = e.target.parentElement.parentElement.value;
}
this.setState({ dropDownItems: '', icon: this.expandMoreButton() });
this.handleOnBlur(e, true, countryValue);
}
expandMoreButton() {
return (
<IconButton
className="expand-more"
onFocus={this.handleFocus}
onBlur={this.handleOnBlur}
name="countryExpand"
src={ExpandMore}
iconAs={Icon}
size="sm"
variant="secondary"
alt="expand-more"
onClick={(e) => { this.handleExpandMore(e); }}
/>
);
}
expandLessButton() {
return (
<IconButton
className="expand-less"
onFocus={this.handleFocus}
onBlur={this.handleOnBlur}
name="countryExpand"
src={ExpandLess}
iconAs={Icon}
size="sm"
variant="secondary"
alt="expand-less"
onClick={(e) => { this.handleExpandLess(e); }}
/>
);
}
render() {
return (
<div className="mb-4">
<FormGroup
as="input"
name={this.props.name}
autoComplete="off"
className="mb-0"
floatingLabel={this.props.floatingLabel}
trailingElement={this.state.icon}
handleChange={this.handleOnChange}
handleBlur={this.handleOnBlur}
handleFocus={this.handleFocus}
value={this.state.displayValue}
errorMessage={this.state.errorMessage}
/>
<div className="dropdown-container">
{ this.state.dropDownItems?.length > 0 ? this.state.dropDownItems : null }
</div>
</div>
);
}
}
CountryDropdown.defaultProps = {
options: null,
floatingLabel: null,
handleFocus: null,
handleBlur: null,
value: null,
errorMessage: null,
errorCode: null,
};
CountryDropdown.propTypes = {
options: PropTypes.arrayOf(PropTypes.object),
floatingLabel: PropTypes.string,
handleFocus: PropTypes.func,
handleBlur: PropTypes.func,
value: PropTypes.string,
errorMessage: PropTypes.string,
errorCode: PropTypes.string,
name: PropTypes.string.isRequired,
};
export default onClickOutside(CountryDropdown);