diff --git a/src/js/components/keyword/KeywordPage.jsx b/src/js/components/keyword/KeywordPage.jsx
new file mode 100644
index 0000000000..396bd86632
--- /dev/null
+++ b/src/js/components/keyword/KeywordPage.jsx
@@ -0,0 +1,116 @@
+/**
+ * KeywordPage.jsx
+ * Created by Lizzie Salita 1/4/18
+ */
+
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import * as MetaTagHelper from 'helpers/metaTagHelper';
+import { InfoCircle } from 'components/sharedComponents/icons/Icons';
+import MetaTags from '../sharedComponents/metaTags/MetaTags';
+import Header from '../sharedComponents/header/Header';
+import Footer from '../sharedComponents/Footer';
+import ResultsTableSection from './table/ResultsTableSection';
+import KeywordHeader from './header/KeywordHeader';
+import KeywordSearchBar from './KeywordSearchBar';
+import KeywordSearchHover from './KeywordSearchHover';
+
+const propTypes = {
+ updateKeyword: PropTypes.func,
+ keywordApplied: PropTypes.bool,
+ summary: PropTypes.object,
+ error: PropTypes.bool,
+ inFlight: PropTypes.bool,
+ results: PropTypes.array,
+ columns: PropTypes.object,
+ sort: PropTypes.object,
+ tableTypes: PropTypes.array,
+ currentType: PropTypes.string,
+ tableInstance: PropTypes.string,
+ switchTab: PropTypes.func,
+ updateSort: PropTypes.func,
+ loadNextPage: PropTypes.func
+};
+
+export default class KeywordPage extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ showHover: false
+ };
+
+ this.showTooltip = this.showTooltip.bind(this);
+ this.closeTooltip = this.closeTooltip.bind(this);
+ }
+
+ showTooltip() {
+ this.setState({
+ showHover: true
+ });
+ }
+
+ closeTooltip() {
+ this.setState({
+ showHover: false
+ });
+ }
+
+ render() {
+ let hover = null;
+ if (this.state.showHover) {
+ hover = (
);
+ }
+ return (
+
+
+
+
+
+
+
+
+
+ Use the Keyword Search to get a broad picture of award data on a given theme.
+ You can search through only award descriptions, or award descriptions plus other
+ attributes.
+
+ {hover}
+
+
+ For a more targeted search, use our
Advanced Search tool,
+ whose extensive filters let you find more precise data sets.
+
+
+
+
+
+
+
+ );
+ }
+}
+
+
+KeywordPage.propTypes = propTypes;
diff --git a/src/js/components/keyword/KeywordSearchBar.jsx b/src/js/components/keyword/KeywordSearchBar.jsx
new file mode 100644
index 0000000000..5ecf916e3f
--- /dev/null
+++ b/src/js/components/keyword/KeywordSearchBar.jsx
@@ -0,0 +1,70 @@
+/**
+ * KeywordSearchBar.jsx
+ * Created by Lizzie Salita 1/5/18
+ */
+
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { Search } from 'components/sharedComponents/icons/Icons';
+
+const propTypes = {
+ submitText: PropTypes.func
+};
+
+export default class KeywordSearchBar extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ searchString: ''
+ };
+
+ this.changedInput = this.changedInput.bind(this);
+ this.searchKeyword = this.searchKeyword.bind(this);
+ }
+
+ searchKeyword(e) {
+ e.preventDefault();
+ if (this.state.searchString.length > 2) {
+ this.props.submitText(this.state.searchString);
+ }
+ }
+
+ changedInput(e) {
+ this.setState({
+ searchString: e.target.value
+ });
+ }
+
+ render() {
+ let disabledClass = 'disabled';
+ if (this.state.searchString.length > 2) {
+ disabledClass = '';
+ }
+ return (
+
+ );
+ }
+}
+
+KeywordSearchBar.propTypes = propTypes;
diff --git a/src/js/components/keyword/KeywordSearchHover.jsx b/src/js/components/keyword/KeywordSearchHover.jsx
new file mode 100644
index 0000000000..7146e39a1b
--- /dev/null
+++ b/src/js/components/keyword/KeywordSearchHover.jsx
@@ -0,0 +1,41 @@
+/**
+ * KeywordSearchHover.jsx
+ * Created by Lizzie Salita 1/16/18
+ */
+
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { InfoCircle, Close } from 'components/sharedComponents/icons/Icons';
+
+const propTypes = {
+ closeTooltip: PropTypes.func
+};
+
+export default class KeywordSearchHover extends React.Component {
+ render() {
+ return (
+
+
+
+
+
+
+ Keyword Search returns results matching recipient name, recipient DUNS, recipient parent DUNS,
+ NAICS code, PSC code, and Award ID. If you’re only interested in results matching award
+ descriptions, tick the box to narrow your search to award description fields only.
+
+
+
+
+ );
+ }
+}
+
+KeywordSearchHover.propTypes = propTypes;
diff --git a/src/js/components/keyword/header/KeywordHeader.jsx b/src/js/components/keyword/header/KeywordHeader.jsx
new file mode 100644
index 0000000000..aceb49ea0a
--- /dev/null
+++ b/src/js/components/keyword/header/KeywordHeader.jsx
@@ -0,0 +1,97 @@
+/**
+ * KeywordHeader.jsx
+ * Created by Lizzie Salita 1/4/18
+ */
+
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import * as MoneyFormatter from 'helpers/moneyFormatter';
+
+const propTypes = {
+ summary: PropTypes.object
+};
+
+export class KeywordHeader extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.generateSummary = this.generateSummary.bind(this);
+ }
+
+ generateSummary() {
+ const primeCount = this.props.summary.primeCount;
+ const primeAmount = this.props.summary.primeAmount;
+
+ const primeCountUnits = MoneyFormatter.calculateUnitForSingleValue(primeCount);
+ const primeAmountUnits = MoneyFormatter.calculateUnitForSingleValue(primeAmount);
+
+ let primeCountPrecision = 0;
+ if (primeCountUnits.unit !== 1) {
+ primeCountPrecision = 1;
+ }
+ const formattedPrimeCount =
+ `${MoneyFormatter.formatNumberWithPrecision(primeCount / primeCountUnits.unit, primeCountPrecision)}${primeCountUnits.unitLabel}`;
+
+ let primeAmountPrecision = 2;
+ if (primeAmountUnits.unit !== 1) {
+ primeAmountPrecision = 1;
+ }
+ const formattedPrimeAmount =
+ `${MoneyFormatter.formatMoneyWithPrecision(primeAmount / primeAmountUnits.unit, primeAmountPrecision)}${primeAmountUnits.unitLabel}`;
+
+ return (
+
+
+ Search Summary
+
+
+
+ Total Prime Award Amount: {formattedPrimeAmount}
+
+
+
+
+ Prime Award Transaction Count: {formattedPrimeCount}
+
+
+
+ );
+ }
+
+ render() {
+ let searchSummary = null;
+ if (this.props.summary) {
+ searchSummary = this.generateSummary();
+ }
+ return (
+
+
+
+
+
Keyword Search
+
+ {searchSummary}
+
+
+
+
+ );
+ }
+}
+
+
+KeywordHeader.propTypes = propTypes;
+
+export default KeywordHeader;
diff --git a/src/js/components/keyword/table/ResultsTable.jsx b/src/js/components/keyword/table/ResultsTable.jsx
new file mode 100644
index 0000000000..e7c69be520
--- /dev/null
+++ b/src/js/components/keyword/table/ResultsTable.jsx
@@ -0,0 +1,139 @@
+/**
+ * ResultsTable.jsx
+ * Created by Lizzie Salita 1/8/18
+ **/
+
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { keywordTableColumnTypes } from 'dataMapping/keyword/keywordTableColumnTypes';
+
+import IBTable from 'components/sharedComponents/IBTable/IBTable';
+
+import ResultsTableFormattedCell from 'components/search/table/cells/ResultsTableFormattedCell';
+import ResultsTableHeaderCell from './cells/ResultsTableHeaderCell';
+
+const propTypes = {
+ results: PropTypes.array,
+ columns: PropTypes.object,
+ visibleWidth: PropTypes.number,
+ loadNextPage: PropTypes.func,
+ currentType: PropTypes.string,
+ tableInstance: PropTypes.string,
+ sort: PropTypes.object,
+ updateSort: PropTypes.func
+};
+
+const rowHeight = 40;
+// setting the table height to a partial row prevents double bottom borders and also clearly
+// indicates when there's more data
+const tableHeight = 29.5 * rowHeight;
+
+export default class ResultsTable extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.headerCellRender = this.headerCellRender.bind(this);
+ this.bodyCellRender = this.bodyCellRender.bind(this);
+ }
+ componentDidMount() {
+ if (this.tableComponent) {
+ this.tableComponent.reloadTable();
+ }
+ }
+ componentDidUpdate(prevProps) {
+ if (prevProps.tableInstance !== this.props.tableInstance) {
+ // table type has changed, reset the scroll
+ if (this.tableComponent) {
+ this.tableComponent.reloadTable();
+ }
+ }
+ }
+
+ headerCellRender(columnIndex) {
+ const columnId = this.props.columns.visibleOrder[columnIndex];
+ const column = this.props.columns.data[columnId];
+ const isLast = (columnIndex + 1) === this.props.columns.visibleOrder.length;
+ const isActive = this.props.sort.field === column.columnName;
+ return (
+
+ );
+ }
+
+ bodyCellRender(columnIndex, rowIndex) {
+ const columnId = this.props.columns.visibleOrder[columnIndex];
+ const cellClass = ResultsTableFormattedCell;
+ const props = {
+ rowIndex,
+ columnIndex,
+ value: this.props.results[rowIndex][columnId],
+ dataType: keywordTableColumnTypes[columnId]
+ };
+
+ return React.createElement(
+ cellClass,
+ props
+ );
+ }
+
+ prepareTable() {
+ let totalWidth = 0;
+
+ const columnOrder = this.props.columns.visibleOrder;
+ const columns = columnOrder.map((columnTitle) => {
+ const column = this.props.columns.data[columnTitle];
+ const columnX = totalWidth;
+ totalWidth += column.width;
+
+ return {
+ x: columnX,
+ width: column.width
+ };
+ });
+
+ return {
+ columns,
+ width: totalWidth
+ };
+ }
+
+ render() {
+ const calculatedValues = this.prepareTable();
+
+ let noResultsClass = '';
+ if (this.props.results.length === 0) {
+ // remove duplicated bottom border
+ noResultsClass = ' no-results';
+ }
+
+ const variableBodyHeight = Math.min(tableHeight, rowHeight * this.props.results.length);
+
+ return (
+
+ {
+ this.tableComponent = table;
+ }} />
+
+ );
+ }
+}
+
+ResultsTable.propTypes = propTypes;
diff --git a/src/js/components/keyword/table/ResultsTableBeginMessage.jsx b/src/js/components/keyword/table/ResultsTableBeginMessage.jsx
new file mode 100644
index 0000000000..fe1af7512b
--- /dev/null
+++ b/src/js/components/keyword/table/ResultsTableBeginMessage.jsx
@@ -0,0 +1,21 @@
+/**
+ * ResultsTableBeginMessage.jsx
+ * Created by Lizzie Salita 1/9/18
+ **/
+
+import React from 'react';
+
+import { CircleArrowUp } from 'components/sharedComponents/icons/Icons';
+
+const ResultsTableBeginMessage = () => (
+
+
+
+
+
+ Enter a keyword to begin your search.
+
+
+);
+
+export default ResultsTableBeginMessage;
diff --git a/src/js/components/keyword/table/ResultsTableSection.jsx b/src/js/components/keyword/table/ResultsTableSection.jsx
new file mode 100644
index 0000000000..2f520b9b5f
--- /dev/null
+++ b/src/js/components/keyword/table/ResultsTableSection.jsx
@@ -0,0 +1,130 @@
+/**
+ * ResultsTableSection.jsx
+ * Created by Lizzie Salita 1/5/18
+ **/
+
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup';
+
+import ResultsTableLoadingMessage from 'components/search/table/ResultsTableLoadingMessage';
+import ResultsTableNoResults from 'components/search/table/ResultsTableNoResults';
+import ResultsTableErrorMessage from 'components/search/table/ResultsTableErrorMessage';
+import ResultsTableBeginMessage from './ResultsTableBeginMessage';
+import ResultsTable from './ResultsTable';
+import ResultsTableTabs from './ResultsTableTabs';
+
+const propTypes = {
+ inFlight: PropTypes.bool,
+ error: PropTypes.bool,
+ keywordApplied: PropTypes.bool,
+ tableTypes: PropTypes.array,
+ currentType: PropTypes.string,
+ switchTab: PropTypes.func,
+ results: PropTypes.array,
+ columns: PropTypes.object,
+ sort: PropTypes.object,
+ updateSort: PropTypes.func,
+ tableInstance: PropTypes.string,
+ loadNextPage: PropTypes.func
+};
+
+export default class ResultsTableSection extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ tableWidth: 0
+ };
+
+ this.setTableWidth = this.setTableWidth.bind(this);
+ }
+ componentDidMount() {
+ // set the initial table width
+ this.setTableWidth();
+ // watch the window for size changes
+ window.addEventListener('resize', this.setTableWidth);
+ }
+
+ componentWillUnmount() {
+ // stop watching for size changes
+ window.removeEventListener('resize', this.setTableWidth);
+ }
+
+ setTableWidth() {
+ const tableWidth = this.tableWidthController.clientWidth - 1;
+ this.setState({ tableWidth });
+ }
+
+ render() {
+ let message = null;
+ let table = (
+
+ );
+
+ if (!this.props.keywordApplied) {
+ table = null;
+ message = (
+
+
+
+ );
+ }
+ else if (this.props.inFlight) {
+ message = (
+
+
+
+ );
+ }
+ else if (this.props.error) {
+ table = null;
+ message = (
+
+
+
+ );
+ }
+ else if (this.props.results.length === 0) {
+ // no results
+ table = null;
+ message = (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ {message}
+
+
{
+ // this is an empty div that scales via CSS
+ // the results table width will follow this div's width
+ this.tableWidthController = div;
+ }} />
+ {table}
+
+
+ );
+ }
+}
+
+ResultsTableSection.propTypes = propTypes;
diff --git a/src/js/components/keyword/table/ResultsTableTabItem.jsx b/src/js/components/keyword/table/ResultsTableTabItem.jsx
new file mode 100644
index 0000000000..4692c2209e
--- /dev/null
+++ b/src/js/components/keyword/table/ResultsTableTabItem.jsx
@@ -0,0 +1,60 @@
+/**
+ * ResultsTableTabItem.jsx
+ * Created by Lizzie Salita 1/11/18
+ **/
+
+import React from 'react';
+import PropTypes from 'prop-types';
+
+const propTypes = {
+ label: PropTypes.string,
+ internal: PropTypes.string,
+ active: PropTypes.bool,
+ enabled: PropTypes.bool,
+ switchTab: PropTypes.func
+};
+
+export default class ResultsTableTabItem extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.clickedTab = this.clickedTab.bind(this);
+ }
+
+ clickedTab() {
+ this.props.switchTab(this.props.internal);
+ }
+
+ render() {
+ let activeClass = '';
+ let disabledStatus = '';
+ if (this.props.active) {
+ activeClass = ' active';
+ }
+ if (this.props.enabled === false) {
+ disabledStatus = true;
+ }
+ else {
+ disabledStatus = false;
+ }
+
+ return (
+
+ );
+ }
+}
+
+ResultsTableTabItem.propTypes = propTypes;
diff --git a/src/js/components/keyword/table/ResultsTableTabs.jsx b/src/js/components/keyword/table/ResultsTableTabs.jsx
new file mode 100644
index 0000000000..1f7b722051
--- /dev/null
+++ b/src/js/components/keyword/table/ResultsTableTabs.jsx
@@ -0,0 +1,37 @@
+/**
+ * ResultsTableTabs.jsx
+ * Created by Lizzie Salita 1/11/18
+ **/
+
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import ResultsTableTabItem from './ResultsTableTabItem';
+
+const propTypes = {
+ types: PropTypes.array,
+ active: PropTypes.string,
+ switchTab: PropTypes.func,
+ disabled: PropTypes.bool
+};
+
+export default class ResultsTableTabs extends React.Component {
+ render() {
+ const tabs = this.props.types.map((type) => (
+
+ ));
+
+ return (
+
+ {tabs}
+
+ );
+ }
+}
+
+ResultsTableTabs.propTypes = propTypes;
diff --git a/src/js/components/keyword/table/cells/ResultsTableHeaderCell.jsx b/src/js/components/keyword/table/cells/ResultsTableHeaderCell.jsx
new file mode 100644
index 0000000000..d47bf08b73
--- /dev/null
+++ b/src/js/components/keyword/table/cells/ResultsTableHeaderCell.jsx
@@ -0,0 +1,114 @@
+/**
+ * ResultsTableHeaderCell.jsx
+ * Created by Lizzie Salita 1/16/18
+ */
+
+import React from 'react';
+import PropTypes from 'prop-types';
+import { ArrowUp, ArrowDown } from 'components/sharedComponents/icons/Icons';
+
+const propTypes = {
+ isLast: PropTypes.bool,
+ isActive: PropTypes.bool,
+ title: PropTypes.string,
+ defaultDirection: PropTypes.string,
+ currentSort: PropTypes.object,
+ updateSort: PropTypes.func,
+ sortDisabled: PropTypes.bool
+};
+
+const TableHeaderCell = (props) => {
+ const clickedSort = (e) => {
+ props.updateSort(props.title, e.target.value);
+ };
+
+ const clickedDefault = () => {
+ // if (props.isActive) {
+ // // toggle the sort direction
+ // let opposite = 'asc';
+ // if (props.currentSort.direction === 'asc') {
+ // opposite = 'desc';
+ // }
+ // props.updateSort(props.title, opposite);
+ // }
+ // else {
+ // props.updateSort(props.title, props.defaultDirection);
+ // }
+ // BODGE: don't allow ascending
+ if (!props.sortDisabled) {
+ props.updateSort(props.title, 'desc');
+ }
+ };
+
+ // keyboard accessible option
+ const pressedKey = (e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ clickedDefault();
+ }
+ };
+
+ let lastClass = '';
+ if (props.isLast) {
+ lastClass = ' last-column';
+ }
+
+ // highlight the active arrows
+ const activeAsc = '';
+ let activeDesc = '';
+
+ if (props.isActive) {
+ activeDesc = ' active';
+ }
+
+ let sortIcons = '';
+ if (!props.sortDisabled) {
+ sortIcons = (
+
+ );
+ }
+
+ /* eslint-disable jsx-a11y/no-noninteractive-tabindex */
+ // allow keyboard selection of the header cell
+ return (
+
+
+
+
+ {props.title}
+
+ {sortIcons}
+
+
+
+ );
+ /* eslint-enable jsx-a11y/no-noninteractive-tabindex */
+};
+
+TableHeaderCell.propTypes = propTypes;
+
+export default TableHeaderCell;
diff --git a/src/js/components/search/SearchPage.jsx b/src/js/components/search/SearchPage.jsx
index 7b027052be..f88ba8f81f 100644
--- a/src/js/components/search/SearchPage.jsx
+++ b/src/js/components/search/SearchPage.jsx
@@ -136,7 +136,7 @@ export default class SearchPage extends React.Component {
}}>
-
+
@@ -159,7 +159,7 @@ export default class SearchPage extends React.Component {
download={this.props.download}
mounted={this.state.showFullDownload}
hideModal={this.hideModal} />
-
+
diff --git a/src/js/components/search/SearchSidebar.jsx b/src/js/components/search/SearchSidebar.jsx
index 6932278987..6d67b12333 100644
--- a/src/js/components/search/SearchSidebar.jsx
+++ b/src/js/components/search/SearchSidebar.jsx
@@ -110,12 +110,15 @@ export default class SearchSidebar extends React.Component {
});
return (
-
+
-
Filters
+ Filters
diff --git a/src/js/components/search/SearchSidebarSubmit.jsx b/src/js/components/search/SearchSidebarSubmit.jsx
index 39c7abf9e0..173b718904 100644
--- a/src/js/components/search/SearchSidebarSubmit.jsx
+++ b/src/js/components/search/SearchSidebarSubmit.jsx
@@ -22,7 +22,10 @@ const SearchSidebarSubmit = (props) => {
}
return (
-
+
);
});
diff --git a/src/js/components/search/filters/agency/SelectedAgencies.jsx b/src/js/components/search/filters/agency/SelectedAgencies.jsx
index 99d818d517..f72bf431b4 100644
--- a/src/js/components/search/filters/agency/SelectedAgencies.jsx
+++ b/src/js/components/search/filters/agency/SelectedAgencies.jsx
@@ -46,7 +46,9 @@ export default class SelectedAgencies extends React.Component {
});
return (
-
+
{shownAgencies}
);
diff --git a/src/js/components/search/filters/agency/ShownAgency.jsx b/src/js/components/search/filters/agency/ShownAgency.jsx
index 2316df03aa..98f7589919 100644
--- a/src/js/components/search/filters/agency/ShownAgency.jsx
+++ b/src/js/components/search/filters/agency/ShownAgency.jsx
@@ -30,9 +30,11 @@ export default class ShownAgency extends React.Component {
);
diff --git a/src/js/components/search/filters/awardAmount/AwardAmountSearch.jsx b/src/js/components/search/filters/awardAmount/AwardAmountSearch.jsx
index 385fc37593..38c81e6b35 100644
--- a/src/js/components/search/filters/awardAmount/AwardAmountSearch.jsx
+++ b/src/js/components/search/filters/awardAmount/AwardAmountSearch.jsx
@@ -9,12 +9,14 @@ import { awardRanges, searchTypes } from 'dataMapping/search/awardAmount';
import * as AwardAmountHelper from 'helpers/awardAmountHelper';
import PrimaryCheckboxType from 'components/sharedComponents/checkbox/PrimaryCheckboxType';
+import SubmitHint from 'components/sharedComponents/filterSidebar/SubmitHint';
import SpecificAwardAmountItem from './SpecificAwardAmountItem';
const propTypes = {
selectAwardRange: PropTypes.func,
awardAmountRanges: PropTypes.object,
- awardAmounts: PropTypes.object
+ awardAmounts: PropTypes.object,
+ dirtyFilters: PropTypes.symbol
};
const defaultProps = {
@@ -29,6 +31,14 @@ export default class AwardAmountSearch extends React.Component {
this.searchSpecificRange = this.searchSpecificRange.bind(this);
}
+ componentDidUpdate(prevProps) {
+ if (this.props.dirtyFilters && prevProps.dirtyFilters !== this.props.dirtyFilters) {
+ if (this.hint) {
+ this.hint.showHint();
+ }
+ }
+ }
+
toggleSelection(selection) {
this.props.selectAwardRange(selection, searchTypes.RANGE);
}
@@ -68,6 +78,10 @@ export default class AwardAmountSearch extends React.Component {
{...this.props}
searchSpecificRange={this.searchSpecificRange} />
+
{
+ this.hint = component;
+ }} />
);
diff --git a/src/js/components/search/filters/awardAmount/SpecificAwardAmountItem.jsx b/src/js/components/search/filters/awardAmount/SpecificAwardAmountItem.jsx
index 41ccc36c6e..ed230ca485 100644
--- a/src/js/components/search/filters/awardAmount/SpecificAwardAmountItem.jsx
+++ b/src/js/components/search/filters/awardAmount/SpecificAwardAmountItem.jsx
@@ -82,7 +82,9 @@ export default class SpecificAwardAmountItem extends React.Component {
return (
-
+
{selectedAwardIDs}
+
{
+ this.hint = component;
+ }} />
);
diff --git a/src/js/components/search/filters/awardID/SelectedAwardIDs.jsx b/src/js/components/search/filters/awardID/SelectedAwardIDs.jsx
index 3cec83a930..27612c8e36 100644
--- a/src/js/components/search/filters/awardID/SelectedAwardIDs.jsx
+++ b/src/js/components/search/filters/awardID/SelectedAwardIDs.jsx
@@ -28,7 +28,9 @@ export default class SelectedAwardIDs extends React.Component {
});
return (
-
+
{shownAwardIDs}
);
diff --git a/src/js/components/search/filters/awardID/ShownAwardID.jsx b/src/js/components/search/filters/awardID/ShownAwardID.jsx
index 6210529fe3..46966f2c85 100644
--- a/src/js/components/search/filters/awardID/ShownAwardID.jsx
+++ b/src/js/components/search/filters/awardID/ShownAwardID.jsx
@@ -18,9 +18,11 @@ export default class ShownAwardID extends React.Component {
);
diff --git a/src/js/components/search/filters/awardType/AwardType.jsx b/src/js/components/search/filters/awardType/AwardType.jsx
index fd7b652184..2dd8d3f260 100644
--- a/src/js/components/search/filters/awardType/AwardType.jsx
+++ b/src/js/components/search/filters/awardType/AwardType.jsx
@@ -8,6 +8,7 @@ import PropTypes from 'prop-types';
import { awardTypeGroups, awardTypeCodes } from 'dataMapping/search/awardType';
import PrimaryCheckboxType from 'components/sharedComponents/checkbox/PrimaryCheckboxType';
+import SubmitHint from 'components/sharedComponents/filterSidebar/SubmitHint';
const defaultProps = {
awardTypes: [
@@ -42,10 +43,18 @@ const defaultProps = {
const propTypes = {
awardTypes: PropTypes.arrayOf(PropTypes.object),
awardType: PropTypes.object,
- bulkTypeChange: PropTypes.func
+ bulkTypeChange: PropTypes.func,
+ dirtyFilters: PropTypes.symbol
};
export default class AwardType extends React.Component {
+ componentDidUpdate(prevProps) {
+ if (this.props.dirtyFilters && prevProps.dirtyFilters !== this.props.dirtyFilters) {
+ if (this.hint) {
+ this.hint.showHint();
+ }
+ }
+ }
render() {
const awardTypes = (
this.props.awardTypes.map((type, index) =>
@@ -66,6 +75,10 @@ export default class AwardType extends React.Component {
+
{
+ this.hint = component;
+ }} />
);
diff --git a/src/js/components/search/filters/cfda/CFDASearch.jsx b/src/js/components/search/filters/cfda/CFDASearch.jsx
index dd4a1303a7..3928e7841f 100644
--- a/src/js/components/search/filters/cfda/CFDASearch.jsx
+++ b/src/js/components/search/filters/cfda/CFDASearch.jsx
@@ -7,15 +7,25 @@ import React from 'react';
import PropTypes from 'prop-types';
import CFDAListContainer from 'containers/search/filters/cfda/CFDAListContainer';
+import SubmitHint from 'components/sharedComponents/filterSidebar/SubmitHint';
import SelectedCFDA from './SelectedCFDA';
const propTypes = {
selectCFDA: PropTypes.func,
removeCFDA: PropTypes.func,
- selectedCFDA: PropTypes.object
+ selectedCFDA: PropTypes.object,
+ dirtyFilters: PropTypes.symbol
};
export default class CFDASearch extends React.Component {
+ componentDidUpdate(prevProps) {
+ if (this.props.dirtyFilters && prevProps.dirtyFilters !== this.props.dirtyFilters) {
+ if (this.hint) {
+ this.hint.showHint();
+ }
+ }
+ }
+
render() {
let selectedCFDA = null;
if (this.props.selectedCFDA.size > 0) {
@@ -29,6 +39,10 @@ export default class CFDASearch extends React.Component {
{selectedCFDA}
+ {
+ this.hint = component;
+ }} />
);
diff --git a/src/js/components/search/filters/cfda/SelectedCFDA.jsx b/src/js/components/search/filters/cfda/SelectedCFDA.jsx
index 384bbf8245..a80cf98f7d 100644
--- a/src/js/components/search/filters/cfda/SelectedCFDA.jsx
+++ b/src/js/components/search/filters/cfda/SelectedCFDA.jsx
@@ -28,7 +28,9 @@ export default class SelectedCFDA extends React.Component {
});
return (
-
+
{shownCFDA}
);
diff --git a/src/js/components/search/filters/contractFilters/ContractFilter.jsx b/src/js/components/search/filters/contractFilters/ContractFilter.jsx
index 11383c9c49..14c291842b 100644
--- a/src/js/components/search/filters/contractFilters/ContractFilter.jsx
+++ b/src/js/components/search/filters/contractFilters/ContractFilter.jsx
@@ -10,12 +10,14 @@ import * as Icons from 'components/sharedComponents/icons/Icons';
import * as ContractFieldDefinitions from 'dataMapping/search/contractFields';
import PrimaryCheckboxType from 'components/sharedComponents/checkbox/PrimaryCheckboxType';
+import SubmitHint from 'components/sharedComponents/filterSidebar/SubmitHint';
const propTypes = {
toggleFilter: PropTypes.func,
contractFilterType: PropTypes.string,
contractFilterOptions: PropTypes.string,
- contractFilterState: PropTypes.string
+ contractFilterState: PropTypes.string,
+ dirtyFilters: PropTypes.symbol
};
const defaultShown = 4;
@@ -36,6 +38,14 @@ export default class ContractFilter extends React.Component {
this.toggleShownAmount = this.toggleShownAmount.bind(this);
}
+ componentDidUpdate(prevProps) {
+ if (this.props.dirtyFilters && prevProps.dirtyFilters !== this.props.dirtyFilters) {
+ if (this.hint) {
+ this.hint.showHint();
+ }
+ }
+ }
+
toggleShownAmount() {
const contractFilters = ContractFieldDefinitions[this.props.contractFilterOptions];
@@ -105,7 +115,9 @@ export default class ContractFilter extends React.Component {
@@ -129,6 +141,10 @@ export default class ContractFilter extends React.Component {
{contractFilterItems}
{toggleButton}
+
{
+ this.hint = component;
+ }} />
);
diff --git a/src/js/components/search/filters/keyword/Keyword.jsx b/src/js/components/search/filters/keyword/Keyword.jsx
index fb4d88450d..ae23ac4f84 100644
--- a/src/js/components/search/filters/keyword/Keyword.jsx
+++ b/src/js/components/search/filters/keyword/Keyword.jsx
@@ -7,7 +7,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Close } from 'components/sharedComponents/icons/Icons';
-
+import SubmitHint from 'components/sharedComponents/filterSidebar/SubmitHint';
import IndividualSubmit from 'components/search/filters/IndividualSubmit';
const propTypes = {
@@ -15,7 +15,8 @@ const propTypes = {
submitText: PropTypes.func,
changedInput: PropTypes.func,
removeKeyword: PropTypes.func,
- value: PropTypes.string
+ value: PropTypes.string,
+ dirtyFilter: PropTypes.string
};
export default class Keyword extends React.Component {
@@ -23,6 +24,15 @@ export default class Keyword extends React.Component {
super(props);
this.searchKeyword = this.searchKeyword.bind(this);
+ this.removeKeyword = this.removeKeyword.bind(this);
+ }
+
+ componentDidUpdate(prevProps) {
+ if (this.props.dirtyFilter && prevProps.dirtyFilter !== this.props.dirtyFilter) {
+ if (this.hint) {
+ this.hint.showHint();
+ }
+ }
}
searchKeyword(e) {
@@ -30,12 +40,24 @@ export default class Keyword extends React.Component {
this.props.submitText();
}
+ removeKeyword() {
+ if (this.searchInput) {
+ // focus on the input field for accessibility users
+ this.searchInput.focus();
+ }
+ this.props.removeKeyword();
+ }
+
render() {
let hideTags = 'hide';
if (this.props.selectedKeyword !== '') {
hideTags = '';
}
+ const accessibility = {
+ 'aria-controls': 'selected-keyword-tags'
+ };
+
return (
-
+
+
{
+ this.hint = component;
+ }} />
diff --git a/src/js/components/search/filters/location/EntityDropdown.jsx b/src/js/components/search/filters/location/EntityDropdown.jsx
index 6139817a3e..2fcd3f43a0 100644
--- a/src/js/components/search/filters/location/EntityDropdown.jsx
+++ b/src/js/components/search/filters/location/EntityDropdown.jsx
@@ -6,6 +6,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import Mousetrap from 'mousetrap';
+import { uniqueId } from 'lodash';
import { AngleDown, AngleUp } from 'components/sharedComponents/icons/Icons';
@@ -37,7 +38,8 @@ export default class EntityDropdown extends React.Component {
this.state = {
expanded: false,
- showWarning: false
+ showWarning: false,
+ warningId: `location-field-warning-${uniqueId()}`
};
this.dropdownRef = null;
@@ -229,7 +231,10 @@ export default class EntityDropdown extends React.Component {
{
this.wrapperDiv = div;
}}>
@@ -242,6 +247,7 @@ export default class EntityDropdown extends React.Component {
aria-haspopup="true"
aria-expanded={this.state.expanded}
aria-owns={`geo-dropdown-${this.props.scope}`}
+ aria-describedby={this.state.warningId}
disabled={!this.props.enabled || this.props.options.length === 0}
ref={(button) => {
this.dropdownButton = button;
@@ -255,7 +261,10 @@ export default class EntityDropdown extends React.Component {
{dropdown}
-
+
diff --git a/src/js/components/search/filters/location/LocationPicker.jsx b/src/js/components/search/filters/location/LocationPicker.jsx
index 48d0e2dddb..4afd20f9ce 100644
--- a/src/js/components/search/filters/location/LocationPicker.jsx
+++ b/src/js/components/search/filters/location/LocationPicker.jsx
@@ -181,6 +181,7 @@ export default class LocationPicker extends React.Component {
diff --git a/src/js/components/search/filters/location/LocationSection.jsx b/src/js/components/search/filters/location/LocationSection.jsx
index 98f5113e3f..fbf71fc37e 100644
--- a/src/js/components/search/filters/location/LocationSection.jsx
+++ b/src/js/components/search/filters/location/LocationSection.jsx
@@ -9,9 +9,12 @@ import PropTypes from 'prop-types';
import POPFilterContainer from 'containers/search/filters/location/POPFilterContainer';
import RecipientFilterContainer from 'containers/search/filters/location/RecipientFilterContainer';
+import SubmitHint from 'components/sharedComponents/filterSidebar/SubmitHint';
+
const propTypes = {
selectedRecipientLocations: PropTypes.object,
- selectedLocations: PropTypes.object
+ selectedLocations: PropTypes.object,
+ dirtyFilters: PropTypes.symbol
};
export default class LocationSection extends React.Component {
@@ -29,6 +32,14 @@ export default class LocationSection extends React.Component {
this.openDefaultTab();
}
+ componentDidUpdate(prevProps) {
+ if (this.props.dirtyFilters && prevProps.dirtyFilters !== this.props.dirtyFilters) {
+ if (this.hint) {
+ this.hint.showHint();
+ }
+ }
+ }
+
openDefaultTab() {
// check if the recipient or place of performance (default) tab should be enabled based
// on the currently selected filters
@@ -66,11 +77,17 @@ export default class LocationSection extends React.Component {
return (
-
+
-
@@ -79,6 +96,10 @@ export default class LocationSection extends React.Component {
@@ -86,6 +107,10 @@ export default class LocationSection extends React.Component {
{filter}
+ {
+ this.hint = component;
+ }} />
);
}
diff --git a/src/js/components/search/filters/location/SelectedLocations.jsx b/src/js/components/search/filters/location/SelectedLocations.jsx
index 0ca125c54a..2dd89b2b5a 100644
--- a/src/js/components/search/filters/location/SelectedLocations.jsx
+++ b/src/js/components/search/filters/location/SelectedLocations.jsx
@@ -28,7 +28,10 @@ export default class SelectedLocations extends React.Component {
});
return (
-
+
{shownLocations}
);
diff --git a/src/js/components/search/filters/location/ShownLocation.jsx b/src/js/components/search/filters/location/ShownLocation.jsx
index 96c435ac2f..4ca400b140 100644
--- a/src/js/components/search/filters/location/ShownLocation.jsx
+++ b/src/js/components/search/filters/location/ShownLocation.jsx
@@ -18,9 +18,11 @@ export default class ShownLocation extends React.Component {
);
diff --git a/src/js/components/search/filters/naics/NAICSSearch.jsx b/src/js/components/search/filters/naics/NAICSSearch.jsx
index afc4596bda..d4c5a98c06 100644
--- a/src/js/components/search/filters/naics/NAICSSearch.jsx
+++ b/src/js/components/search/filters/naics/NAICSSearch.jsx
@@ -7,15 +7,25 @@ import React from 'react';
import PropTypes from 'prop-types';
import NAICSListContainer from 'containers/search/filters/naics/NAICSListContainer';
+import SubmitHint from 'components/sharedComponents/filterSidebar/SubmitHint';
import SelectedNAICS from './SelectedNAICS';
const propTypes = {
selectNAICS: PropTypes.func,
removeNAICS: PropTypes.func,
- selectedNAICS: PropTypes.object
+ selectedNAICS: PropTypes.object,
+ dirtyFilters: PropTypes.symbol
};
export default class NAICSSearch extends React.Component {
+ componentDidUpdate(prevProps) {
+ if (this.props.dirtyFilters && prevProps.dirtyFilters !== this.props.dirtyFilters) {
+ if (this.hint) {
+ this.hint.showHint();
+ }
+ }
+ }
+
render() {
let selectedNAICS = null;
if (this.props.selectedNAICS.size > 0) {
@@ -29,6 +39,10 @@ export default class NAICSSearch extends React.Component {
{selectedNAICS}
+ {
+ this.hint = component;
+ }} />
);
diff --git a/src/js/components/search/filters/naics/SelectedNAICS.jsx b/src/js/components/search/filters/naics/SelectedNAICS.jsx
index 869c6c718d..f32cb48a91 100644
--- a/src/js/components/search/filters/naics/SelectedNAICS.jsx
+++ b/src/js/components/search/filters/naics/SelectedNAICS.jsx
@@ -28,7 +28,9 @@ export default class SelectedNAICS extends React.Component {
});
return (
-
+
{shownNAICS}
);
diff --git a/src/js/components/search/filters/otherFilters/ShownValue.jsx b/src/js/components/search/filters/otherFilters/ShownValue.jsx
index 82e858f265..0e52c8c22f 100644
--- a/src/js/components/search/filters/otherFilters/ShownValue.jsx
+++ b/src/js/components/search/filters/otherFilters/ShownValue.jsx
@@ -18,9 +18,11 @@ export default class ShownValue extends React.Component {
);
diff --git a/src/js/components/search/filters/psc/PSCSearch.jsx b/src/js/components/search/filters/psc/PSCSearch.jsx
index 595965f697..562106d555 100644
--- a/src/js/components/search/filters/psc/PSCSearch.jsx
+++ b/src/js/components/search/filters/psc/PSCSearch.jsx
@@ -7,15 +7,25 @@ import React from 'react';
import PropTypes from 'prop-types';
import PSCListContainer from 'containers/search/filters/psc/PSCListContainer';
+import SubmitHint from 'components/sharedComponents/filterSidebar/SubmitHint';
import SelectedPSC from './SelectedPSC';
const propTypes = {
selectPSC: PropTypes.func,
removePSC: PropTypes.func,
- selectedPSC: PropTypes.object
+ selectedPSC: PropTypes.object,
+ dirtyFilters: PropTypes.symbol
};
export default class PSCSearch extends React.Component {
+ componentDidUpdate(prevProps) {
+ if (this.props.dirtyFilters && prevProps.dirtyFilters !== this.props.dirtyFilters) {
+ if (this.hint) {
+ this.hint.showHint();
+ }
+ }
+ }
+
render() {
let selectedPSC = null;
if (this.props.selectedPSC.size > 0) {
@@ -29,6 +39,10 @@ export default class PSCSearch extends React.Component {
{selectedPSC}
+
{
+ this.hint = component;
+ }} />
);
diff --git a/src/js/components/search/filters/psc/SelectedPSC.jsx b/src/js/components/search/filters/psc/SelectedPSC.jsx
index 3bf4d63acb..03f3135bf4 100644
--- a/src/js/components/search/filters/psc/SelectedPSC.jsx
+++ b/src/js/components/search/filters/psc/SelectedPSC.jsx
@@ -29,7 +29,9 @@ export default class SelectedPSC extends React.Component {
});
return (
-
+
{shownPSC}
);
diff --git a/src/js/components/search/filters/recipient/RecipientSearch.jsx b/src/js/components/search/filters/recipient/RecipientSearch.jsx
index 25941f3e9a..ae11946216 100644
--- a/src/js/components/search/filters/recipient/RecipientSearch.jsx
+++ b/src/js/components/search/filters/recipient/RecipientSearch.jsx
@@ -7,14 +7,24 @@ import PropTypes from 'prop-types';
import RecipientNameDUNSContainer from
'containers/search/filters/recipient/RecipientNameDUNSContainer';
+import SubmitHint from 'components/sharedComponents/filterSidebar/SubmitHint';
import SelectedRecipients from './SelectedRecipients';
const propTypes = {
toggleRecipient: PropTypes.func,
- selectedRecipients: PropTypes.object
+ selectedRecipients: PropTypes.object,
+ dirtyFilters: PropTypes.symbol
};
export default class RecipientSearch extends React.Component {
+ componentDidUpdate(prevProps) {
+ if (this.props.dirtyFilters && prevProps.dirtyFilters !== this.props.dirtyFilters) {
+ if (this.hint) {
+ this.hint.showHint();
+ }
+ }
+ }
+
render() {
let selectedRecipients = null;
@@ -31,6 +41,10 @@ export default class RecipientSearch extends React.Component {
{...this.props}
toggleRecipient={this.props.toggleRecipient} />
{selectedRecipients}
+
{
+ this.hint = component;
+ }} />
);
diff --git a/src/js/components/search/filters/recipient/RecipientType.jsx b/src/js/components/search/filters/recipient/RecipientType.jsx
index a7c43d15e0..41bd51861d 100644
--- a/src/js/components/search/filters/recipient/RecipientType.jsx
+++ b/src/js/components/search/filters/recipient/RecipientType.jsx
@@ -8,6 +8,7 @@ import PropTypes from 'prop-types';
import { recipientTypes, recipientTypeGroups } from 'dataMapping/search/recipientType';
import PrimaryCheckboxType from 'components/sharedComponents/checkbox/PrimaryCheckboxType';
+import SubmitHint from 'components/sharedComponents/filterSidebar/SubmitHint';
const defaultProps = {
recipientTypeMapping: [
@@ -62,10 +63,19 @@ const defaultProps = {
const propTypes = {
recipientTypeMapping: PropTypes.arrayOf(PropTypes.object),
- selectedTypes: PropTypes.object
+ selectedTypes: PropTypes.object,
+ dirtyFilters: PropTypes.symbol
};
export default class RecipientType extends React.Component {
+ componentDidUpdate(prevProps) {
+ if (this.props.dirtyFilters && prevProps.dirtyFilters !== this.props.dirtyFilters) {
+ if (this.hint) {
+ this.hint.showHint();
+ }
+ }
+ }
+
render() {
const checkboxTypes =
this.props.recipientTypeMapping.map((type, index) =>
@@ -86,6 +96,10 @@ export default class RecipientType extends React.Component {
+
{
+ this.hint = component;
+ }} />