department-of-veterans-affairs/vets-website

View on GitHub
src/applications/edu-benefits/feedback-tool/components/SchoolSelectField.jsx

Summary

Maintainability
D
2 days
Test Coverage
import React from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';
import classNames from 'classnames';
import {
  VaPagination,
  VaLoadingIndicator,
  VaCheckbox,
  VaRadio,
  VaRadioOption,
} from '@department-of-veterans-affairs/component-library/dist/react-bindings';
import { connect } from 'react-redux';

import { Element } from 'platform/utilities/scroll';

import {
  clearSearch,
  restoreFromPrefill,
  searchInputChange,
  selectInstitution,
  searchSchools,
  toggleManualSchoolEntry,
} from '../actions/schoolSearch';
import {
  selectCurrentPageNumber,
  selectFacilityCodeErrorMessages,
  selectFormSubmitted,
  selectInstitutionQuery,
  selectInstitutions,
  selectInstitutionSelected,
  selectManualSchoolEntryChecked,
  selectPagesCount,
  selectSearchInputValue,
  selectSearchResultsCount,
  selectShowInstitutions,
  selectShowInstitutionsLoading,
  selectShowNoResultsFound,
  selectShowPagination,
  selectShowPaginationLoading,
  selectShowSearchResults,
} from '../selectors/schoolSearch';
import {
  displaySingleLineAddress,
  transformSearchToolAddress,
} from '../helpers';

export class SchoolSelectField extends React.Component {
  constructor(props) {
    super(props);
    this.debouncedSearchInstitutions = _.debounce(
      value => this.props.searchSchools(value),
      150,
    );
  }

  componentDidMount() {
    // hydrate search if restoring from SiP
    // if there is a search term stored in the form data
    // if the search term in the form data isn't already in the redux state and displayed
    const institutionSelected = this.props.formData['view:institutionSelected'];
    const searchTermToRestore = this.props.formData['view:institutionQuery'];
    const pageNumberToRestore = this.props.formData['view:currentPageNumber'];
    if (
      searchTermToRestore &&
      searchTermToRestore !== this.props.institutionQuery &&
      !this.props.showInstitutions
    ) {
      this.props.restoreFromPrefill({
        institutionSelected,
        institutionQuery: searchTermToRestore,
        page: pageNumberToRestore,
        searchInputValue: searchTermToRestore,
      });
    }
  }

  componentWillUnmount() {
    this.debouncedSearchInstitutions.cancel();
  }

  handleManualSchoolEntryToggled = currentValue => {
    this.props.toggleManualSchoolEntry(currentValue);
    this.props.onChange({
      ...this.props.formData,
      'view:manualSchoolEntryChecked': currentValue,
    });
  };

  handleOptionClick = institution => {
    const {
      address1,
      address2,
      address3,
      city,
      country,
      facilityCode,
      name,
      state,
      zip,
    } = institution;
    this.props.selectInstitution({
      address1,
      address2,
      address3,
      city,
      facilityCode,
      name,
      state,
    });
    const address = transformSearchToolAddress({
      address1,
      address2,
      address3,
      city,
      country,
      state,
      zip,
    });
    this.props.onChange({
      ...this.props.formData,
      name,
      facilityCode,
      address,
    });
  };

  handlePageSelect = e => {
    const { page } = e.detail;
    this.resultCount.focus();
    this.debouncedSearchInstitutions({
      institutionQuery: this.props.institutionQuery,
      page,
    });
    this.props.onChange({
      ...this.props.formData,
      'view:currentPageNumber': page,
    });
  };

  handleSearchInputKeyDown = e => {
    if ((e.which || e.keyCode) === 13) {
      e.preventDefault();
      e.target.blur();

      this.debouncedSearchInstitutions({
        institutionQuery: this.props.searchInputValue,
      });

      this.props.onChange({
        ...this.props.formData,
        'view:manualSchoolEntryChecked': false,
        'view:institutionQuery': this.props.searchInputValue,
      });
    }
  };

  handleSearchClick = e => {
    e.preventDefault();
    this.props.onChange({
      ...this.props.formData,
      name: null,
      facilityCode: null,
      address: {},
      'view:manualSchoolEntryChecked': false,
      'view:institutionQuery': this.props.searchInputValue,
    });
    this.debouncedSearchInstitutions({
      institutionQuery: this.props.searchInputValue,
    });
  };

  handleSearchInputChange = e => {
    let searchInputValue;
    if (typeof e === 'string') {
      searchInputValue = e;
    } else {
      searchInputValue = e.target.value;
    }

    this.props.searchInputChange({ searchInputValue });
  };

  handleStartOver = e => {
    e.preventDefault();

    this.props.onChange({});
    this.props.clearSearch();
    this.searchInput.focus();
  };

  render() {
    const {
      currentPageNumber,
      errorMessages,
      facilityCodeSelected,
      formContext,
      institutionQuery,
      institutions,
      institutionSelected,
      manualSchoolEntryChecked,
      pagesCount,
      searchInputValue,
      searchResultsCount,
      showErrors,
      showInstitutions,
      showInstitutionsLoading,
      showNoResultsFound,
      showPagination,
      showPaginationLoading,
      showSearchResults,
    } = this.props;

    const fieldsetClassNames = classNames('search-select-school-fieldset');
    const schoolSearchClassNames = classNames('school-search', {
      'usa-input-error': showErrors,
    });
    const labelClassNames = classNames('school-search-label', {
      'usa-input-error-label': showErrors,
    });

    if (formContext.reviewMode && manualSchoolEntryChecked) {
      return null;
    }

    if (formContext.reviewMode) {
      const {
        address1,
        address2,
        address3,
        city,
        country,
        name,
        state,
      } = institutionSelected;

      return (
        <div>
          {name && <p>{name}</p>}
          {address1 && <p>{address1}</p>}
          {address2 && <p>{address2}</p>}
          {address3 && <p>{address3}</p>}
          {(city || state) && (
            <p>{`${city}${city && state && ', '}${state}`}</p>
          )}
          {!city && !state && <p>{country}</p>}
        </div>
      );
    }

    return (
      <fieldset className={fieldsetClassNames}>
        <div>
          <div className={schoolSearchClassNames}>
            <label
              id="school-search-label"
              className={labelClassNames}
              htmlFor="school-search-input"
            >
              School Information
              {!manualSchoolEntryChecked && (
                <span className="schemaform-required-span">(*Required)</span>
              )}
            </label>
            {showErrors && (
              <span
                className="usa-input-error-message"
                role="alert"
                id="facility-code-error-message"
              >
                {errorMessages.map((message, index) => (
                  <span key={index}>
                    <span className="sr-only">Error</span>
                    {!showInstitutions
                      ? message
                      : `Select a school on this page. If you can't find your school, select the box to enter the school's name and address.`}
                  </span>
                ))}
              </span>
            )}
            <span>
              Enter your school’s name or city to search for your school
            </span>
            <div className="search-controls">
              <Element name="schoolSearch" />
              <div className="search-input">
                <input
                  id="school-search-input"
                  name="school-search-input"
                  onChange={this.handleSearchInputChange}
                  onKeyDown={this.handleSearchInputKeyDown}
                  ref={input => {
                    this.searchInput = input;
                  }}
                  type="text"
                  value={searchInputValue}
                />
                <button
                  className="search-schools-button usa-button-primary"
                  onClick={this.handleSearchClick}
                >
                  Search Schools
                </button>
              </div>
              <div className="clear-search vads-u-margin-top--1p5">
                <va-button
                  secondary
                  onClick={this.handleStartOver}
                  text="Reset Search"
                />
              </div>
            </div>
          </div>
          <VaCheckbox
            checked={manualSchoolEntryChecked}
            onVaChange={event =>
              this.handleManualSchoolEntryToggled(event.detail.checked)
            }
            label="I want to type in my school’s name and address."
          />
          <div aria-live="polite" aria-relevant="additions text">
            {showSearchResults &&
              searchResultsCount > 0 && (
                <div
                  className="search-results-count"
                  // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
                  tabIndex="0"
                  ref={el => {
                    this.resultCount = el;
                  }}
                >
                  {`${searchResultsCount} results for ${institutionQuery}`}
                </div>
              )}
            {showSearchResults &&
              showInstitutions && (
                <VaRadio
                  className="school-select-field-radio"
                  onVaValueChange={e =>
                    this.handleOptionClick(
                      _.find(institutions, { facilityCode: e?.detail?.value }),
                    )
                  }
                >
                  {institutions.map(institution => {
                    const {
                      address1,
                      address2,
                      address3,
                      city,
                      country,
                      facilityCode,
                      name,
                      state,
                      zip,
                    } = institution;
                    return (
                      <VaRadioOption
                        name={`page-${currentPageNumber}-radio-option`}
                        className="school-select-field-radio-option"
                        key={facilityCode}
                        value={facilityCode}
                        checked={facilityCode === facilityCodeSelected}
                        label={name}
                        tile
                        description={displaySingleLineAddress({
                          address1,
                          address2,
                          address3,
                          city,
                          country,
                          state,
                          zip,
                        }).toUpperCase()}
                      />
                    );
                  })}
                </VaRadio>
              )}{' '}
            {showSearchResults &&
              showInstitutionsLoading && (
                <div>
                  <VaLoadingIndicator
                    message={`Searching ${institutionQuery}...`}
                  />
                </div>
              )}
            {showSearchResults &&
              showNoResultsFound && (
                <div className="no-results-box">
                  <p>
                    <strong>We can’t find your school</strong>
                    <br />
                    We’re sorry. We can’t find any school that matches your
                    entry. Please try entering a different school name or city.
                    Or, you can check the box to enter your school information
                    yourself.
                  </p>
                </div>
              )}
            {showSearchResults &&
              showPaginationLoading && (
                <div>
                  <VaLoadingIndicator
                    message={`Loading page ${currentPageNumber} results for ${institutionQuery}...`}
                  />
                </div>
              )}
          </div>
          {showSearchResults &&
            showPagination && (
              <VaPagination
                page={currentPageNumber}
                pages={pagesCount}
                ariaLabelSuffix="of school results"
                onPageSelect={this.handlePageSelect}
                maxPageListLength={10}
                showLastPage
              />
            )}
        </div>
      </fieldset>
    );
  }
}

export const mapStateToProps = (state, ownProps) => {
  const currentPageNumber = selectCurrentPageNumber(state);
  const errorMessages = selectFacilityCodeErrorMessages(ownProps);
  const facilityCodeSelected = ownProps.formData
    ? ownProps.formData.facilityCode
    : '';
  const institutionQuery = selectInstitutionQuery(state);
  const institutions = selectInstitutions(state);
  const institutionSelected = selectInstitutionSelected(state);
  const manualSchoolEntryChecked =
    selectManualSchoolEntryChecked(state) || false;
  const pagesCount = selectPagesCount(state);
  const searchInputValue = selectSearchInputValue(state);
  const searchResultsCount = selectSearchResultsCount(state);
  const showErrors =
    errorMessages.length > 0 &&
    selectFormSubmitted(ownProps) &&
    !manualSchoolEntryChecked;
  const showInstitutions = selectShowInstitutions(state);
  const showInstitutionsLoading = selectShowInstitutionsLoading(state);
  const showNoResultsFound = selectShowNoResultsFound(state);
  const showPagination = selectShowPagination(state);
  const showPaginationLoading = selectShowPaginationLoading(state);
  const showSearchResults = selectShowSearchResults(state);

  return {
    currentPageNumber,
    errorMessages,
    facilityCodeSelected,
    institutionQuery,
    institutions,
    institutionSelected,
    manualSchoolEntryChecked,
    pagesCount,
    searchInputValue,
    searchResultsCount,
    showErrors,
    showInstitutions,
    showInstitutionsLoading,
    showNoResultsFound,
    showPagination,
    showPaginationLoading,
    showSearchResults,
  };
};
const mapDispatchToProps = {
  clearSearch,
  restoreFromPrefill,
  searchInputChange,
  searchSchools,
  selectInstitution,
  toggleManualSchoolEntry,
};

SchoolSelectField.propTypes = {
  currentPageNumber: PropTypes.number,
  errorMessages: PropTypes.array,
  facilityCodeSelected: PropTypes.string,
  institutionQuery: PropTypes.string,
  institutions: PropTypes.array,
  institutionSelected: PropTypes.oneOfType([
    PropTypes.string,
    PropTypes.object,
  ]),
  manualSchoolEntryChecked: PropTypes.bool,
  pagesCount: PropTypes.number,
  searchInputValue: PropTypes.string,
  searchResultsCount: PropTypes.number,
  showErrors: PropTypes.bool,
  showInstitutions: PropTypes.bool.isRequired,
  showInstitutionsLoading: PropTypes.bool.isRequired,
  showNoResultsFound: PropTypes.bool.isRequired,
  showPagination: PropTypes.bool.isRequired,
  showPaginationLoading: PropTypes.bool.isRequired,
  showSearchResults: PropTypes.bool.isRequired,
};

SchoolSelectField.defaultProps = {
  showInstitutions: false,
  showInstitutionsLoading: false,
  showNoResultsFound: false,
  showPagination: false,
  showPaginationLoading: false,
  showSearchResults: true,
};

export default connect(
  mapStateToProps,
  mapDispatchToProps,
)(SchoolSelectField);