diff --git a/.eslintrc b/.eslintrc index 0f06080..810e05d 100644 --- a/.eslintrc +++ b/.eslintrc @@ -10,7 +10,8 @@ 1, { "extensions": [".js", ".jsx"], } - ] + ], + "func-names": "off" }, "env": { "browser": true diff --git a/package.json b/package.json index 25f231d..2cdf3bc 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "postinstall": "patch-package" }, "dependencies": { + "classnames": "^2.2.5", "he": "1.2.0", "intl": "^1.2.5", "patch-package": "^6.2.0", diff --git a/src/components/current-query/index.js b/src/components/current-query/index.js index c720e7a..c91c348 100644 --- a/src/components/current-query/index.js +++ b/src/components/current-query/index.js @@ -5,21 +5,25 @@ import moment from 'moment'; import { LiveMessenger } from 'react-aria-live'; import helpers from '../../helpers'; - // Create dumb component which can be configured by props. -const FacetType = props => ( - ); +FacetType.propTypes = { + id: PropTypes.string.isRequired, + onClick: PropTypes.string.isRequired, + children: PropTypes.node.isRequired, +}; // Configure and render the FacetType component to render as list facet type. -class ListFacetType extends React.Component { - removeListFacetValue(field, values, value) { - this.props.announcePolite(`Removed ${field.value} filter.`); +const ListFacetType = function ({ searchField, announcePolite, onChange }) { + function removeListFacetValue(field, values, value) { + announcePolite(`Removed ${field.value} filter.`); const { foundIdx, @@ -47,64 +51,74 @@ class ListFacetType extends React.Component { } // Send query based on new state. - this.props.onChange(field, values.filter((v, i) => i !== foundIdx)); + onChange(field, values.filter((v, i) => i !== foundIdx)); } } - render() { - const { searchField } = this.props; - return (searchField.value.map((val, i) => ( - this.removeListFacetValue(searchField.field, searchField.value, val)} - > - {/* Add spacing to hierarchical facet values: Type>Term = Type > Term. */} - {val.replace('>', ' > ')} - - ))); - } -} + return (searchField.value.map((val) => ( + removeListFacetValue(searchField.field, searchField.value, val)} + > + {/* Add spacing to hierarchical facet values: Type>Term = Type > Term. */} + {val.replace('>', ' > ')} + + ))); +}; +ListFacetType.propTypes = { + announcePolite: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + searchField: PropTypes.shape({ + field: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + }).isRequired, +}; // Configure and render the FacetType component to render as range facet type. -class RangeFacetType extends React.Component { - removeRangeFacetValue(field) { - this.props.announcePolite(`Removed ${field.value} filter.`); - this.props.onChange(field, []); +const RangeFacetType = function ({ searchField, announcePolite, onChange }) { + function removeRangeFacetValue(field) { + announcePolite(`Removed ${field.value} filter.`); + onChange(field, []); } - render() { - const { searchField } = this.props; - // Create a moment from the search start date. - const start = moment(searchField.value[0]); - // Use UTC. - start.utc(); - // Create a formatted string from start date. - const startFormatted = start.format('MM/DD/YYYY'); - // Create a moment from search end date. - const end = moment(searchField.value[1]); - // Use utc. - end.utc(); - // Create a formatted string from end date. - const endFormatted = end.format('MM/DD/YYYY'); - // Determine if we chose the same or different start / end dates. - const diff = start.diff(end, 'days'); - // Only show the start date if the same date were chosen, otherwise: start - end. - const filterValue = diff ? `${startFormatted} - ${endFormatted}` : startFormatted; - return ( - this.removeRangeFacetValue(searchField.field)}> - {filterValue} - - ); - } -} + // Create a moment from the search start date. + const start = moment(searchField.value[0]); + // Use UTC. + start.utc(); + // Create a formatted string from start date. + const startFormatted = start.format('MM/DD/YYYY'); + // Create a moment from search end date. + const end = moment(searchField.value[1]); + // Use utc. + end.utc(); + // Create a formatted string from end date. + const endFormatted = end.format('MM/DD/YYYY'); + // Determine if we chose the same or different start / end dates. + const diff = start.diff(end, 'days'); + // Only show the start date if the same date were chosen, otherwise: start - end. + const filterValue = diff ? `${startFormatted} - ${endFormatted}` : startFormatted; + return ( + removeRangeFacetValue(searchField.field)}> + {filterValue} + + ); +}; +RangeFacetType.propTypes = { + announcePolite: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + searchField: PropTypes.shape({ + value: PropTypes.arrayOf(PropTypes.string).isRequired, + field: PropTypes.string.isRequired, + }).isRequired, +}; // Configure and render the FacetType component to render as text facet type. -class TextFacetType extends React.Component { - removeTextValue(field) { - this.props.announcePolite(`Removed search term ${field.value}.`); +const TextFacetType = function ({ searchField, announcePolite, onChange }) { + function removeTextValue(field) { + announcePolite(`Removed search term ${field.value}.`); // Setting this to '' or "" throws a fatal error. - this.props.onChange(field, null); + onChange(field, null); // Get current querystring params. const parsed = queryString.parse(window.location.search); // Remove the search term param, if it exists. @@ -127,67 +141,72 @@ class TextFacetType extends React.Component { } } - render() { - const { searchField } = this.props; - return ( - this.removeTextValue(searchField.field)}> - {searchField.value} - - ); - } -} - -class FederatedCurrentQuery extends React.Component { - render() { - const { query } = this.props; - - const fields = query.searchFields.filter(searchField => searchField.value - && searchField.value.length > 0); - - // Create a map of known facet type child components which can be rendered dynamically. - const facetTypes = { - 'list-facet': ListFacetType, - 'range-facet': RangeFacetType, - text: TextFacetType, - }; - - return ( - - {({ announcePolite }) => ( - - {fields.length > 0 && // Only render this if there are filters applied. -
-

- Currently Applied Search Filters. -

-

- Click a filter to remove it from your search query. -

- {/* Only render the values for visible facets / filters */} - {fields.filter(searchField => !searchField.isHidden).map((searchField, i) => { - // Determine which child component to render. - const MyFacetType = facetTypes[searchField.type]; - return ( - - ); - })} -
- } -
- )} -
- ); - } -} + return ( + removeTextValue(searchField.field)}> + {searchField.value} + + ); +}; +TextFacetType.propTypes = { + announcePolite: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + searchField: PropTypes.shape({ + field: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + }).isRequired, +}; +const FederatedCurrentQuery = (props) => { + const { onChange, query } = props; + + const fields = query.searchFields.filter((searchField) => searchField.value + && searchField.value.length > 0); + + // Create a map of known facet type child components which can be rendered dynamically. + const facetTypes = { + 'list-facet': ListFacetType, + 'range-facet': RangeFacetType, + text: TextFacetType, + }; + + return ( + + {({ announcePolite }) => ( + <> + {fields.length > 0 // Only render this if there are filters applied. + && ( +
+

+ Currently Applied Search Filters. +

+

+ Click a filter to remove it from your search query. +

+ {/* Only render the values for visible facets / filters */} + {fields.filter((searchField) => !searchField.isHidden).map((searchField) => { + // Determine which child component to render. + const MyFacetType = facetTypes[searchField.type]; + return ( + + ); + })} +
+ )} + + )} +
+ ); +}; FederatedCurrentQuery.propTypes = { - onChange: PropTypes.func, - query: PropTypes.object, + onChange: PropTypes.func.isRequired, + query: PropTypes.shape({ + searchFields: PropTypes.arrayOf(PropTypes.object), + }).isRequired, }; export default FederatedCurrentQuery; diff --git a/src/components/federated-solr-faceted-search.js b/src/components/federated-solr-faceted-search.js index 19810c5..25240da 100644 --- a/src/components/federated-solr-faceted-search.js +++ b/src/components/federated-solr-faceted-search.js @@ -3,14 +3,17 @@ import PropTypes from 'prop-types'; import { LiveAnnouncer } from 'react-aria-live'; import FederatedSolrComponentPack from './federated_solr_component_pack'; import helpers from '../helpers'; -//import componentPack from "./component-pack"; -const getFacetValues = (type, results, field, lowerBound, upperBound) => { - return type === 'period-range-facet' - ? (results.facets[lowerBound] || []).concat(results.facets[upperBound] || []) - : type === 'list-facet' || type === 'range-facet' - ? results.facets[field] || [] - : null; +const getFacetValues = function (type, results, field, lowerBound, upperBound) { + let values = null; + + if (type === 'period-range-facet') { + values = (results.facets[lowerBound] || []).concat(results.facets[upperBound] || []); + } else if (type === 'list-facet' || type === 'range-facet') { + values = results.facets[field] || []; + } + + return values; }; class FederatedSolrFacetedSearch extends React.Component { @@ -21,24 +24,28 @@ class FederatedSolrFacetedSearch extends React.Component { } resetFilters() { - let { query } = this.props; + const { query, onSearchFieldChange } = this.props; let searchTerm = ''; // Keep only the value of the main search field. - for (const field of query.searchFields) { + query.searchField.map((field) => { + const newField = field; + if (field.field !== query.mainQueryField) { // Remove the field value. - delete (field.value); + delete (newField.value); // Collapse the sidebar filter toggle. - field.collapse = true; + newField.collapse = true; // Collapse the terms sidebar filter toggle. if (Object.hasOwnProperty.call(field, 'expandedHierarchies')) { - field.expandedHierarchies = []; + newField.expandedHierarchies = []; } } else { // Extract the value of the main search term to use when setting new URL for this state. searchTerm = field.value; } - } + + return newField; + }); // Set new parsed params based on only search term value. const parsed = { search: searchTerm, @@ -46,10 +53,8 @@ class FederatedSolrFacetedSearch extends React.Component { // Add new url to browser window history. helpers.qs.addNewUrlToBrowserHistory(parsed); - // Update state to remove the filter field values. - this.setState({ query }); // Execute search. - this.props.onSearchFieldChange(); + onSearchFieldChange(); } render() { @@ -64,6 +69,8 @@ class FederatedSolrFacetedSearch extends React.Component { onTextInputChange, onSortFieldChange, onPageChange, + sidebarFilters, + onSelectDoc, } = this.props; const { searchFields, sortFields, rows } = query; const start = query.start ? query.start : 0; @@ -84,9 +91,11 @@ class FederatedSolrFacetedSearch extends React.Component { ? () : null; - const pagination = query.pageStrategy === 'paginate' ? - : - null; + /* eslint-disable react/jsx-props-no-spreading */ + + const pagination = query.pageStrategy === 'paginate' + ? + : null; const preloadListItem = query.pageStrategy === 'cursor' && results.docs.length < results.numFound @@ -94,8 +103,8 @@ class FederatedSolrFacetedSearch extends React.Component { : null; let pageTitle; - if (this.props.options.pageTitle != null) { - pageTitle =

{this.props.options.pageTitle}

; + if (options.pageTitle != null) { + pageTitle =

{options.pageTitle}

; } return ( @@ -105,14 +114,14 @@ class FederatedSolrFacetedSearch extends React.Component { {/* Only render the visible facets / filters. Note: their values may still be used in the query, if they were pre-set. */} {searchFields - .filter(searchField => this.props.sidebarFilters.indexOf(searchField.field) > -1 + .filter((searchField) => sidebarFilters.indexOf(searchField.field) > -1 && !searchField.isHidden) - .map((searchField, i) => { + .map((searchField) => { const { type, field, @@ -124,7 +133,7 @@ class FederatedSolrFacetedSearch extends React.Component { return ( ); - }) - } + })}
@@ -147,7 +155,7 @@ class FederatedSolrFacetedSearch extends React.Component { label="Enter search term:" onSuggest={onTextInputChange} onChange={onSearchFieldChange} - value={searchFields.find(sf => sf.field === 'tm_rendered_item').value} + value={searchFields.find((sf) => sf.field === 'tm_rendered_item').value} />
-

sf.field === 'tm_rendered_item').value || this.props.options.showEmptySearchResults) ? 'solr-search-results-container__prompt fs-element-invisible' : 'solr-search-results-container__prompt'}>{this.props.options.searchPrompt || 'Please enter a search term.'}

-
sf.field === 'tm_rendered_item').value || this.props.options.showEmptySearchResults) ? 'solr-search-results-container__wrapper' : 'solr-search-results-container__wrapper fs-element-invisible'}> +

sf.field === 'tm_rendered_item').value || options.showEmptySearchResults) ? 'solr-search-results-container__prompt fs-element-invisible' : 'solr-search-results-container__prompt'}>{options.searchPrompt || 'Please enter a search term.'}

+
sf.field === 'tm_rendered_item').value || options.showEmptySearchResults) ? 'solr-search-results-container__wrapper' : 'solr-search-results-container__wrapper fs-element-invisible'}> sf.field === 'tm_rendered_item').value} + noResultsText={options.noResults || null} + termValue={searchFields.find((sf) => sf.field === 'tm_rendered_item').value} /> {resultPending} @@ -181,12 +189,12 @@ class FederatedSolrFacetedSearch extends React.Component { doc={doc} fields={searchFields} key={doc.id || i} - onSelect={this.props.onSelectDoc} + onSelect={onSelectDoc} resultIndex={i} rows={rows} start={start} highlight={results.highlighting[doc.id]} - hostname={this.props.options.hostname} + hostname={options.hostname} /> ))} {preloadListItem} @@ -198,6 +206,8 @@ class FederatedSolrFacetedSearch extends React.Component {
); + + /* eslint-enable react/jsx-props-no-spreading */ } } @@ -220,19 +230,49 @@ FederatedSolrFacetedSearch.defaultProps = { FederatedSolrFacetedSearch.propTypes = { bootstrapCss: PropTypes.bool, - customComponents: PropTypes.object, - onCsvExport: PropTypes.func, - onNewSearch: PropTypes.func, - onPageChange: PropTypes.func, + customComponents: PropTypes.shape(FederatedSolrComponentPack), + pageStrategy: PropTypes.string, + rows: PropTypes.number, + searchFields: PropTypes.arrayOf(PropTypes.shape({ + type: PropTypes.string, + field: PropTypes.string, + })), + sortFields: PropTypes.arrayOf(PropTypes.shape({ + label: PropTypes.string, + field: PropTypes.string, + })), + onCsvExport: PropTypes.func.isRequired, + onNewSearch: PropTypes.func.isRequired, + onPageChange: PropTypes.func.isRequired, onSearchFieldChange: PropTypes.func.isRequired, - onTextInputChange: PropTypes.func, - onSelectDoc: PropTypes.func, + onTextInputChange: PropTypes.func.isRequired, + onSelectDoc: PropTypes.func.isRequired, onSortFieldChange: PropTypes.func.isRequired, - query: PropTypes.object, - results: PropTypes.object, + query: PropTypes.shape({ + searchField: PropTypes.arrayOf(PropTypes.object), + searchFields: PropTypes.arrayOf(PropTypes.object), + mainQueryField: PropTypes.string, + sortFields: PropTypes.arrayOf(PropTypes.shape({ + label: PropTypes.string, + field: PropTypes.string, + })), + rows: PropTypes.number, + start: PropTypes.number, + pageStrategy: PropTypes.string, + }).isRequired, + results: PropTypes.shape(FederatedSolrComponentPack.results).isRequired, showCsvExport: PropTypes.bool, truncateFacetListsAt: PropTypes.number, - options: PropTypes.object, + options: PropTypes.shape({ + pageTitle: PropTypes.string, + autocomplete: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]), + showEmptySearchResults: PropTypes.bool, + searchPrompt: PropTypes.string, + noResults: PropTypes.string, + hostname: PropTypes.string, + }), + sidebarFilters: PropTypes.arrayOf(PropTypes.string), + }; export default FederatedSolrFacetedSearch; diff --git a/src/components/federated_solr_component_pack.js b/src/components/federated_solr_component_pack.js index c558215..56f7a37 100644 --- a/src/components/federated_solr_component_pack.js +++ b/src/components/federated_solr_component_pack.js @@ -1,36 +1,36 @@ // Create a custom component pack from the default component pack -import {defaultComponentPack} from "../solr-faceted-search-react/src/index"; -import FederatedResult from "./results/result"; -import FederatedTextSearch from "./text-search/index"; -import FederatedListFacet from "./list-facet/index"; -import FederatedRangeFacet from "./range-facet/index"; -import FederatedSearchFieldContainer from "./search-field-container"; -import FederatedResultList from "./results/list"; -import FederatedPagination from "./results/pagination"; -import FederatedCountLabel from "./results/count-label"; -import FederatedCurrentQuery from "./current-query"; -import FederatedSortMenu from "./sort-menu"; +import { defaultComponentPack } from '../solr-faceted-search-react/src/index'; +import FederatedResult from './results/result'; +import FederatedTextSearch from './text-search/index'; +import FederatedListFacet from './list-facet/index'; +import FederatedRangeFacet from './range-facet/index'; +import FederatedSearchFieldContainer from './search-field-container'; +import FederatedResultList from './results/list'; +import FederatedPagination from './results/pagination'; +import FederatedCountLabel from './results/count-label'; +import FederatedCurrentQuery from './current-query'; +import FederatedSortMenu from './sort-menu'; const FederatedSolrComponentPack = { ...defaultComponentPack, searchFields: { ...defaultComponentPack.searchFields, text: FederatedTextSearch, - "list-facet": FederatedListFacet, - "range-facet": FederatedRangeFacet, + 'list-facet': FederatedListFacet, + 'range-facet': FederatedRangeFacet, container: FederatedSearchFieldContainer, - currentQuery: FederatedCurrentQuery + currentQuery: FederatedCurrentQuery, }, results: { ...defaultComponentPack.results, result: FederatedResult, list: FederatedResultList, paginate: FederatedPagination, - resultCount: FederatedCountLabel + resultCount: FederatedCountLabel, }, sortFields: { - menu: FederatedSortMenu - } -} + menu: FederatedSortMenu, + }, +}; export default FederatedSolrComponentPack; diff --git a/src/components/icons/chevrons.js b/src/components/icons/chevrons.js index 8749b4d..ca9088d 100644 --- a/src/components/icons/chevrons.js +++ b/src/components/icons/chevrons.js @@ -3,22 +3,33 @@ * @todo consider using an svg loader package (i.e. https://www.npmjs.com/package/react-svg-loader) to import and return the svg elements directly vs duplicating markup */ -import React from "react"; +import React from 'react'; const DoubleChevronLeft = () => ( - + + + + ); const ChevronLeft = () => ( - + ); const ChevronRight = () => ( - + ); -const DoubleChevronRight = () => ( - +const DoubleChevronRight = () => ( + + + + ); -export {DoubleChevronLeft, ChevronLeft, ChevronRight, DoubleChevronRight}; +export { + DoubleChevronLeft, + ChevronLeft, + ChevronRight, + DoubleChevronRight, +}; diff --git a/src/components/icons/search.js b/src/components/icons/search.js index 8031f4f..66ee822 100644 --- a/src/components/icons/search.js +++ b/src/components/icons/search.js @@ -1,11 +1,9 @@ -import React from "react"; +import React from 'react'; -class Search extends React.Component { - render() { - return ( - - ); - } -} +const Search = () => ( + + + +); -export default Search; \ No newline at end of file +export default Search; diff --git a/src/components/list-facet/index.js b/src/components/list-facet/index.js index 28155c0..9aad3d4 100644 --- a/src/components/list-facet/index.js +++ b/src/components/list-facet/index.js @@ -5,7 +5,6 @@ import AnimateHeight from 'react-animate-height'; import helpers from '../../helpers'; class FederatedListFacet extends React.Component { - constructor(props) { super(props); @@ -15,16 +14,18 @@ class FederatedListFacet extends React.Component { }; } - handleClick(value) { + handleClick(clickValue) { + const { field, value, onChange } = this.props; + const { foundIdx, parsed, isQsParamField, param, } = helpers.qs.getFieldQsInfo({ - field: this.props.field, - values: this.props.value, - value, + field, + values: value, + value: clickValue, }); // Define var for new parsed qs params object. @@ -38,35 +39,35 @@ class FederatedListFacet extends React.Component { if (param) { // Add value to parsed qs params. newParsed = helpers.qs.addValueToQsParam({ - field: this.props.field, - value, + field, + value: clickValue, param, parsed, }); } else { // If there is not already a qs param for this field value. // Add new qs param for field + value. newParsed = helpers.qs.addQsParam({ - field: this.props.field, - value, + field, + value: clickValue, parsed, }); } // Send new query based on app state. - this.props.onChange(this.props.field, this.props.value.concat(value)); + onChange(field, value.concat(clickValue)); } else { // If the click is removing this field value. // If their is already a qs param for this field value. if (param) { newParsed = helpers.qs.removeValueFromQsParam({ - field: this.props.field, - value, + field, + value: clickValue, param, parsed, }); } // Send new query based on app state. - this.props.onChange(this.props.field, this.props.value.filter((v, i) => i !== foundIdx)); + onChange(field, value.filter((v, i) => i !== foundIdx)); } helpers.qs.addNewUrlToBrowserHistory(newParsed); @@ -74,18 +75,25 @@ class FederatedListFacet extends React.Component { } toggleExpand(hierarchyFacetValue) { - this.props.onSetCollapse(this.props.field, !(this.props.collapse || false)); + const { + onSetCollapse, + field, + collapse, + expandedHierarchies, + } = this.props; + + onSetCollapse(field, !collapse); // If this is a hierarchical list facet. if (hierarchyFacetValue) { // Determine the current state of the expanded hierarchical list facets. - const indexOfExpandedHierarchyFacetValue = this.props.expandedHierarchies + const indexOfExpandedHierarchyFacetValue = expandedHierarchies .indexOf(hierarchyFacetValue); if (indexOfExpandedHierarchyFacetValue > -1) { // This accordion is currently expanded, so collapse it. - this.props.expandedHierarchies.splice(indexOfExpandedHierarchyFacetValue,1); + expandedHierarchies.splice(indexOfExpandedHierarchyFacetValue, 1); } else { // This accordion is currently collapsed, so expand it. - this.props.expandedHierarchies.push(hierarchyFacetValue); + expandedHierarchies.push(hierarchyFacetValue); } } } @@ -100,9 +108,9 @@ class FederatedListFacet extends React.Component { hierarchy, options, } = this.props; - const { truncateFacetListsAt } = this.state; + const { truncateFacetListsAt, filter } = this.state; - const siteList = options.siteList; + const { siteList } = options; const facetCounts = facets.filter((facet, i) => i % 2 === 1); const facetValues = facets.filter((facet, i) => i % 2 === 0); // Create an object of facets {value: count} to keep consistent for inputs. @@ -120,15 +128,14 @@ class FederatedListFacet extends React.Component { if (value.length < 1 && Object.keys(facetInputs).length < 2) { return null; } - } - else { + } else { facetValues.forEach((v, i) => { const key = facetValues[i]; facetInputs[key] = facetCounts[i]; }); } - const expanded = !(collapse || false); + const expanded = !collapse; const height = expanded ? 'auto' : 0; // If we need to generate multiple list-fact accordion groups @@ -151,6 +158,8 @@ class FederatedListFacet extends React.Component { // } const terms = {}; facetValues.forEach((facetValue, i) => { + const { expandedHierarchies } = this.props; + // Create array of [Type, Term] from Type>Term. const pieces = facetValue.split('>'); types.push(pieces[0]); @@ -158,7 +167,7 @@ class FederatedListFacet extends React.Component { if (!Object.hasOwnProperty.call(terms, pieces[0])) { terms[pieces[0]] = {}; terms[pieces[0]].items = []; - terms[pieces[0]].expanded = (this.props.expandedHierarchies.indexOf(pieces[0]) > -1); + terms[pieces[0]].expanded = (expandedHierarchies.indexOf(pieces[0]) > -1); terms[pieces[0]].height = terms[pieces[0]].expanded ? 'auto' : 0; } // Add the object for this facet value to the array of terms for this type. @@ -178,31 +187,41 @@ class FederatedListFacet extends React.Component { const listFacetHierarchyLis = []; // Define array of checkbox Lis which we'll populate with react fragments, per type. const listFacetHierarchyTermsLis = []; + + /* eslint-disable react/no-array-index-key */ + // Iterate through types (accordion lis). uniqueTypes.forEach((type, i) => { // Populate the checkbox lis react fragments for each type. listFacetHierarchyTermsLis[type] = []; - terms[type].items.forEach((termObj, i) => termObj.facetCount - && listFacetHierarchyTermsLis[type].push(
  • - -
  • )); + terms[type].items.forEach((termObj, j) => termObj.facetCount + && listFacetHierarchyTermsLis[type].push( +
  • + {/* eslint jsx-a11y/label-has-associated-control: ["error", { assert: "either" } ] */} + +
  • , + )); // Populate the accordion lis array with all of its checkboxes. + /* eslint no-unused-expressions: [2, { allowShortCircuit: true }] */ listFacetHierarchyTermsLis[type].length && listFacetHierarchyLis.push(
  • - Toggle filter group for {type} + Toggle filter group for + {type}
    -
  • ); + , + ); }); + + /* eslint-enable react/no-array-index-key */ + // Render the group of accordion lis with their facet value checkbox lists. return listFacetHierarchyLis; } @@ -233,6 +257,7 @@ class FederatedListFacet extends React.Component { return (
  • - Toggle filter group for {label} + Toggle filter group for + {label}
    {facetValues.filter((facetValue, i) => facetInputs[facetValue] > 0 && (truncateFacetListsAt < 0 || i < truncateFacetListsAt)) - .map((facetValue, i) => { - if (this.state.filter.length === 0 - || facetValue.toLowerCase().indexOf(this.state.filter.toLowerCase()) > -1) { + .map((facetValue) => { + if (filter.length === 0 + || facetValue.toLowerCase().indexOf(filter.toLowerCase()) > -1) { return (
  • @@ -285,24 +313,28 @@ FederatedListFacet.defaultProps = { hierarchy: false, expandedHierarchies: [], value: [], + collapse: false, }; FederatedListFacet.propTypes = { - bootstrapCss: PropTypes.bool, - children: PropTypes.array, collapse: PropTypes.bool, - expandedHierarchies: PropTypes.array, - facetSort: PropTypes.string, - facets: PropTypes.array.isRequired, + expandedHierarchies: PropTypes.arrayOf(PropTypes.string), + facets: PropTypes.arrayOf( + PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + ).isRequired, field: PropTypes.string.isRequired, hierarchy: PropTypes.bool, - label: PropTypes.string, - onChange: PropTypes.func, - onFacetSortChange: PropTypes.func, - onSetCollapse: PropTypes.func, - query: PropTypes.object, - truncateFacetListsAt: PropTypes.number, - value: PropTypes.array, + label: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, + onSetCollapse: PropTypes.func.isRequired, + truncateFacetListsAt: PropTypes.number.isRequired, + value: PropTypes.arrayOf(PropTypes.number), + options: PropTypes.shape({ + siteList: PropTypes.oneOfType([PropTypes.bool, PropTypes.shape({ + length: PropTypes.number, + indexOf: PropTypes.func, + })]), + }).isRequired, }; export default FederatedListFacet; diff --git a/src/components/range-facet/index.js b/src/components/range-facet/index.js index d9542ea..24097a8 100644 --- a/src/components/range-facet/index.js +++ b/src/components/range-facet/index.js @@ -1,78 +1,85 @@ import PropTypes from 'prop-types'; -import React from "react"; -import cx from "classnames"; -import moment from "moment"; +import React from 'react'; +import cx from 'classnames'; +import moment from 'moment'; import 'react-dates/initialize'; import { DateRangePicker } from 'react-dates'; import AnimateHeight from 'react-animate-height'; class FederatedRangeFacet extends React.Component { - constructor(props) { super(props); this.state = { - filter: "", - truncateFacetListsAt: props.truncateFacetListsAt, startDate: null, endDate: null, focusedInput: null, }; } - toggleExpand() { - this.props.onSetCollapse(this.props.field, !(this.props.collapse || false)); + // See: https://reactjs.org/docs/react-component.html#the-component-lifecycle + componentDidUpdate(nextProps) { + const { value } = this.props; + // Clear component inputs when rangeFacet value transitions from populated->empty. + if (value.length && !nextProps.value.length) { + /* eslint-disable react/no-did-update-set-state */ + this.setState({ + startDate: null, + endDate: null, + }); + /* eslint-enable react/no-did-update-set-state */ + } + } + + handleDatesChange(startDate, endDate) { + const { onChange, field } = this.props; + this.setState({ startDate, endDate }); + // If there are no start/end dates, something has just cleared them, so update props. + if (startDate === null && endDate === null) { + onChange(field, []); + } } handleCalendarClose(value) { + const { onChange, field } = this.props; // If there are not start/end dates, we've likely just cleared them, so update props. if (value.startDate !== null && value.endDate !== null) { // The default time is noon, so start date should start at midnight. - const momentToSolrStart = moment(value.startDate).subtract({hours:12}).format("YYYY-MM-DDTHH:mm:ss") + 'Z'; + const momentToSolrStart = `${moment(value.startDate).subtract({ hours: 12 }).format('YYYY-MM-DDTHH:mm:ss')}Z`; // The default time is noon, so end date should end at 11:59:59. - const momentToSolrEnd = moment(value.endDate).add({hours:11, minutes:59, seconds: 59}).format("YYYY-MM-DDTHH:mm:ss") + 'Z'; - this.props.onChange(this.props.field, [momentToSolrStart, momentToSolrEnd]); + const momentToSolrEnd = `${moment(value.endDate).add({ hours: 11, minutes: 59, seconds: 59 }).format('YYYY-MM-DDTHH:mm:ss')}Z`; + onChange(field, [momentToSolrStart, momentToSolrEnd]); } } - handleDatesChange(startDate,endDate) { - this.setState({startDate, endDate}); - // If there are no start/end dates, something has just cleared them, so update props. - if (startDate === null && endDate === null) { - this.props.onChange(this.props.field, []); - } - } - - // See: https://reactjs.org/docs/react-component.html#the-component-lifecycle - componentDidUpdate(nextProps) { - // Clear component inputs when rangeFacet value transitions from populated->empty. - if (this.props.value.length && !nextProps.value.length) { - this.setState({ - startDate: null, - endDate: null - }); - } + toggleExpand() { + const { onSetCollapse, field, collapse } = this.props; + onSetCollapse(field, !collapse); } render() { - const {label, facets, field, collapse } = this.props; + const { + label, facets, field, collapse, + } = this.props; + + const { startDate, endDate, focusedInput } = this.state; - const expanded = !(collapse || false); + const expanded = !collapse; const height = expanded ? 'auto' : 0; // Set better react date props for responsive behavior. // See: https://github.com/airbnb/react-dates/issues/262 - let calendarOrientation = undefined; // prop will not be added unless set. - let calendarFullScreen = undefined; // prop will not be added unless set. + let calendarOrientation; // prop will not be added unless set. + let calendarFullScreen; // prop will not be added unless set. let calendarMonths = 2; // view 2 months on large screens // When viewing 2 months, the last month should be the current. let getLastMonth = () => moment().subtract(1, 'months'); // Set prop values for mobile. - if (window.matchMedia("(max-width: 600px)").matches) { + if (window.matchMedia('(max-width: 600px)').matches) { /* the viewport is less than 600 pixels wide */ calendarMonths = 1; - calendarOrientation = "vertical"; + calendarOrientation = 'vertical'; calendarFullScreen = true; getLastMonth = undefined; // prop will not be added on mobile. } @@ -80,12 +87,15 @@ class FederatedRangeFacet extends React.Component { return (
  • {if (event.keyCode === 13) {this.toggleExpand()}}} - >{label}
    + onKeyDown={(event) => { if (event.keyCode === 13) { this.toggleExpand(); } }} + > + {label} +
  • {/* See: https://github.com/airbnb/react-dates#daterangepicker */} this.handleDatesChange(startDate,endDate)} // PropTypes.func.isRequired, - focusedInput={this.state.focusedInput} // PropTypes.oneOf([START_DATE, END_DATE]) or null, - onFocusChange={focusedInput => this.setState({ focusedInput })} // PropTypes.func.isRequired, + // We need to rename the destructured date variables, because they are already used + // in the outer scope. + // See https://wesbos.com/destructuring-renaming + onDatesChange={({ + startDate: newStartDate, + endDate: newEndDate, + }) => this.handleDatesChange( + newStartDate, + newEndDate, + )} // PropTypes.func.isRequired, + focusedInput={focusedInput} // PropTypes.oneOf([START_DATE, END_DATE]) or null, + onFocusChange={(newFocusedInput) => this.setState( + { focusedInput: newFocusedInput }, + )} // PropTypes.func.isRequired, isOutsideRange={(day) => { const today = moment().format('YYYY-MM-DD'); - return day.diff(today, 'days') > 0 || moment(day).isBefore(facets[0]) + return day.diff(today, 'days') > 0 || moment(day).isBefore(facets[0]); }} // allow only past dates & dates after earliest facet value minimumNights={0} // allow just 1 day (same start/end date) - small={true} // use the smaller theme + small // use the smaller theme showClearDates // show the clear dates button - onClose={(value)=> this.handleCalendarClose(value)} + onClose={(value) => this.handleCalendarClose(value)} // custom phrases for screenreader phrases={{ - calendarLabel: "Calendar", + calendarLabel: 'Calendar', chooseAvailableStartDate: ({ date }) => `Choose ${date} as your search filter start date.`, chooseAvailableEndDate: ({ date }) => `Choose ${date} as your search filter end date.`, - clearDates: "Clear Dates", - closeDatePicker: "Close", + clearDates: 'Clear Dates', + closeDatePicker: 'Close', dateIsSelected: ({ date }) => `You have selected ${date}.`, dateIsUnavailable: ({ date }) => `Sorry, ${date} is unavailable.`, - enterKey: "Enter key", - escape: "Escape key", - focusStartDate: "Interact with the calendar and add the check-in date for your trip.", - hideKeyboardShortcutsPanel: "Close the shortcuts panel.", - homeEnd: "Home and end keys", - jumpToNextMonth: "Move forward to switch to the next month.", - jumpToPrevMonth: "Move backward to switch to the previous month.", - keyboardNavigationInstructions: "Press the down arrow key to interact with the calendar and\n select a date. Press the question mark key to get the keyboard shortcuts for changing dates.", - keyboardShortcuts: "Keyboard Shortcuts", - leftArrowRightArrow: "Right and left arrow keys", - moveFocusByOneDay: "Move backward (left) and forward (right) by one day.", - moveFocusByOneMonth: "Switch months.", - moveFocusByOneWeek: "Move backward (up) and forward (down) by one week.", - moveFocustoStartAndEndOfWeek: "Go to the first or last day of a week.", - openThisPanel: "Open this panel.", - pageUpPageDown: "page up and page down keys", - questionMark: "Question mark", - returnFocusToInput: "Return to the date input field.", - selectFocusedDate: "Select the date in focus.", - showKeyboardShortcutsPanel: "Open the keyboard shortcuts panel.", - upArrowDownArrow: "up and down arrow keys" + enterKey: 'Enter key', + escape: 'Escape key', + focusStartDate: 'Interact with the calendar and add the check-in date for your trip.', + hideKeyboardShortcutsPanel: 'Close the shortcuts panel.', + homeEnd: 'Home and end keys', + jumpToNextMonth: 'Move forward to switch to the next month.', + jumpToPrevMonth: 'Move backward to switch to the previous month.', + keyboardNavigationInstructions: 'Press the down arrow key to interact with the calendar and\n select a date. Press the question mark key to get the keyboard shortcuts for changing dates.', + keyboardShortcuts: 'Keyboard Shortcuts', + leftArrowRightArrow: 'Right and left arrow keys', + moveFocusByOneDay: 'Move backward (left) and forward (right) by one day.', + moveFocusByOneMonth: 'Switch months.', + moveFocusByOneWeek: 'Move backward (up) and forward (down) by one week.', + moveFocustoStartAndEndOfWeek: 'Go to the first or last day of a week.', + openThisPanel: 'Open this panel.', + pageUpPageDown: 'page up and page down keys', + questionMark: 'Question mark', + returnFocusToInput: 'Return to the date input field.', + selectFocusedDate: 'Select the date in focus.', + showKeyboardShortcutsPanel: 'Open the keyboard shortcuts panel.', + upArrowDownArrow: 'up and down arrow keys', }} // > mobile only props initialVisibleMonth={getLastMonth} // large viewports only @@ -156,23 +177,20 @@ class FederatedRangeFacet extends React.Component { } FederatedRangeFacet.defaultProps = { - value: [] + value: [], + collapse: false, }; FederatedRangeFacet.propTypes = { - bootstrapCss: PropTypes.bool, - children: PropTypes.array, collapse: PropTypes.bool, - facetSort: PropTypes.string, - facets: PropTypes.array.isRequired, + facets: PropTypes.arrayOf( + PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + ).isRequired, field: PropTypes.string.isRequired, - label: PropTypes.string, - onChange: PropTypes.func, - onFacetSortChange: PropTypes.func, - onSetCollapse: PropTypes.func, - query: PropTypes.object, - truncateFacetListsAt: PropTypes.number, - value: PropTypes.array + label: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, + onSetCollapse: PropTypes.func.isRequired, + value: PropTypes.arrayOf(PropTypes.number), }; export default FederatedRangeFacet; diff --git a/src/components/results/count-label.js b/src/components/results/count-label.js index 9217ff3..9c331f4 100644 --- a/src/components/results/count-label.js +++ b/src/components/results/count-label.js @@ -9,17 +9,45 @@ function searchResultsStat(currentPage, numFound, rows, pageAmt, noResultsText, if (numFound > rows) { // Many pages a11yMessage = `Showing page ${currentPage + 1} of ${pageAmt} (${numFound} results).`; message = ( - Showing page - {currentPage + 1} of - {pageAmt} ({numFound} results). + + Showing page + + {' '} + {currentPage + 1} + + {' '} + of + + {' '} + {pageAmt} + + {' '} + ( + {numFound} + {' '} + results). ); } else if (numFound <= rows && numFound > 1) { // Single page a11yMessage = `Showing ${numFound} results.`; - message = (Showing {numFound} results.); + message = ( + + {'Showing '} + {numFound} + {' '} + results. + + ); } else if (numFound === 1) { // Single item a11yMessage = `Showing ${numFound} result.`; - message = (Showing {numFound} result.); + message = ( + + {'Showing '} + {numFound} + {' '} + result. + + ); } else if (numFound === 0) { // No results message = noResultsText || 'Sorry, your search yielded no results.'; a11yMessage = message; @@ -27,40 +55,40 @@ function searchResultsStat(currentPage, numFound, rows, pageAmt, noResultsText, // Don't announce total results when wildcard query sent on term clear. a11yMessage = termValue ? a11yMessage : ''; return ( - + <>

    {message}

    -
    + ); } -class FederatedCountLabel extends React.Component { - render() { - const { - numFound, - start, - rows, - noResultsText, - termValue, - } = this.props; - const currentPage = start / rows; - const pageAmt = Math.ceil(numFound / rows); - return ( - - {searchResultsStat(currentPage, numFound, rows, pageAmt, noResultsText, termValue)} - - ); - } -} +const FederatedCountLabel = function ({ + numFound, + start, + rows, + noResultsText, + termValue, +}) { + const currentPage = start / rows; + const pageAmt = Math.ceil(numFound / rows); + return ( + <> + {searchResultsStat(currentPage, numFound, rows, pageAmt, noResultsText, termValue)} + + ); +}; FederatedCountLabel.propTypes = { numFound: PropTypes.number.isRequired, start: PropTypes.number.isRequired, - rows: PropTypes.number, + rows: PropTypes.number.isRequired, + noResultsText: PropTypes.string, + termValue: PropTypes.string, }; FederatedCountLabel.defaultProps = { - start: 0, + noResultsText: '', + termValue: '', }; export default FederatedCountLabel; diff --git a/src/components/results/list.js b/src/components/results/list.js index 1ab4220..5655f01 100644 --- a/src/components/results/list.js +++ b/src/components/results/list.js @@ -1,22 +1,21 @@ import PropTypes from 'prop-types'; -import React from "react"; +import React from 'react'; -class FederatedResultList extends React.Component { +const FederatedResultList = ({ children }) => ( + <> +

    Search results

    +
      + { children } +
    + +); - render() { - return ( - -

    Search results

    -
      - {this.props.children} -
    -
    - ); - } -} +FederatedResultList.defaultProps = { + children: [], +}; FederatedResultList.propTypes = { - children: PropTypes.array + children: PropTypes.arrayOf(PropTypes.array), }; export default FederatedResultList; diff --git a/src/components/results/pagination.js b/src/components/results/pagination.js index 79c964e..9f65b2f 100644 --- a/src/components/results/pagination.js +++ b/src/components/results/pagination.js @@ -1,16 +1,19 @@ import PropTypes from 'prop-types'; -import React from "react"; -import cx from "classnames"; -import {DoubleChevronLeft, ChevronLeft, ChevronRight, DoubleChevronRight} from "../icons/chevrons"; +import React from 'react'; +import cx from 'classnames'; +import { + DoubleChevronLeft, ChevronLeft, ChevronRight, DoubleChevronRight, +} from '../icons/chevrons'; class FederatedPagination extends React.Component { - onPageChange(page, pageAmt) { + const { onChange } = this.props; + if (page >= pageAmt || page < 0) { return; } - this.props.onChange(page); + onChange(page); - if(document.getElementById("stat") != null) { - document.getElementById("stat").focus({preventScroll: false}); + if (document.getElementById('stat') != null) { + document.getElementById('stat').focus({ preventScroll: false }); } } @@ -21,78 +24,79 @@ class FederatedPagination extends React.Component { }; renderPage(page, currentPage, key) { - let isCurrentPage = page === currentPage; + const isCurrentPage = page === currentPage; return ( -
  • -
  • ); } render() { - const { query, results } = this.props; + const { query, results, options } = this.props; const { start, rows } = query; const { numFound } = results; const pageAmt = Math.ceil(numFound / rows); const currentPage = start / rows; - const numButtons = this.props.options.paginationButtons || 5; + const numButtons = options.paginationButtons; let rangeStart = currentPage - 2 < 0 ? 0 : currentPage - 2; - let rangeEnd = rangeStart + numButtons > pageAmt ? pageAmt : rangeStart + numButtons; + const rangeEnd = rangeStart + numButtons > pageAmt ? pageAmt : rangeStart + numButtons; if (rangeEnd - rangeStart < numButtons && rangeStart > 0) { rangeStart = rangeEnd - numButtons; if (rangeStart < 0) { rangeStart = 0; } } - let pages = []; - for (let page = rangeStart; page < rangeEnd; page++) { + const pages = []; + for (let page = rangeStart; page < rangeEnd; page += 1) { if (pages.indexOf(page) < 0) { pages.push(page); } } - let firstPageHidden = (currentPage === 0); - let prevPageHidden = (currentPage - 1 < 0); - let nextPageHidden = (currentPage + 1 >= pageAmt); - let lastPageHidden = (pageAmt === 0 || currentPage === pageAmt - 1); + const firstPageHidden = (currentPage === 0); + const prevPageHidden = (currentPage - 1 < 0); + const nextPageHidden = (currentPage + 1 >= pageAmt); + const lastPageHidden = (pageAmt === 0 || currentPage === pageAmt - 1); return (