From 8e31ca032c893c3bd937f203dc0eb779f5ed657e Mon Sep 17 00:00:00 2001
From: Vim <86254807+vim-usds@users.noreply.github.com>
Date: Fri, 3 Dec 2021 10:56:15 -0500
Subject: [PATCH] [Draft] Adds Nominatum search behind a feature flag (#935)
* Add intial search component
* Add nominatum simple
* Connect search field to Nominatum API
- remove react-query
- remove react-query logic from J40Map
- move searchHandler to MapSearch
* Adjust zoom and territory focus
- adjust zoom buttons in CSS to allow for search field
* Place search behind a feature flag
* Add cors to fetch and error handling
- this is to test on OMB machines
* Add error messaging and bound search results to US
- adjust controls to add error message to search
- add MapSearchMessage component for error message
- add unit tests
- add state to track if API results are empty
- add intl on two strings, placeholder and error message
* Remove warpper around MapSearch component
- reorder component import in J40Map
- remove unused CSS in MapSearch.module.scss
- remove and comment on wrapper error on MapSearch
- rename isSearchEmpty to isSearchResultsEmpty
- update snapshot
* Add error message
- if the search query returns null, show an error message
---
.../LegacyTests/mapZoomLatLong.spec.js | 2 +-
client/src/components/J40Map.module.scss | 5 +-
client/src/components/J40Map.tsx | 24 ++++++-
.../MapSearch/MapSearch.module.scss | 9 +++
.../MapSearch/MapSearch.module.scss.d.ts | 13 ++++
.../components/MapSearch/MapSearch.test.tsx | 18 +++++
client/src/components/MapSearch/MapSearch.tsx | 72 +++++++++++++++++++
.../__snapshots__/MapSearch.test.tsx.snap | 43 +++++++++++
client/src/components/MapSearch/index.tsx | 3 +
.../MapSearchMessage.module.scss | 18 +++++
.../MapSearchMessage.module.scss.d.ts | 14 ++++
.../MapSearchMessage.test.tsx | 28 ++++++++
.../MapSearchMessage/MapSearchMessage.tsx | 21 ++++++
.../MapSearchMessage.test.tsx.snap | 11 +++
.../src/components/MapSearchMessage/index.tsx | 3 +
client/src/components/layout.tsx | 1 +
.../territoryFocusControl.module.scss | 6 +-
client/src/data/copy/explore.tsx | 10 +++
18 files changed, 294 insertions(+), 7 deletions(-)
create mode 100644 client/src/components/MapSearch/MapSearch.module.scss
create mode 100644 client/src/components/MapSearch/MapSearch.module.scss.d.ts
create mode 100644 client/src/components/MapSearch/MapSearch.test.tsx
create mode 100644 client/src/components/MapSearch/MapSearch.tsx
create mode 100644 client/src/components/MapSearch/__snapshots__/MapSearch.test.tsx.snap
create mode 100644 client/src/components/MapSearch/index.tsx
create mode 100644 client/src/components/MapSearchMessage/MapSearchMessage.module.scss
create mode 100644 client/src/components/MapSearchMessage/MapSearchMessage.module.scss.d.ts
create mode 100644 client/src/components/MapSearchMessage/MapSearchMessage.test.tsx
create mode 100644 client/src/components/MapSearchMessage/MapSearchMessage.tsx
create mode 100644 client/src/components/MapSearchMessage/__snapshots__/MapSearchMessage.test.tsx.snap
create mode 100644 client/src/components/MapSearchMessage/index.tsx
diff --git a/client/cypress/integration/LegacyTests/mapZoomLatLong.spec.js b/client/cypress/integration/LegacyTests/mapZoomLatLong.spec.js
index 4e723d31..46ca7903 100644
--- a/client/cypress/integration/LegacyTests/mapZoomLatLong.spec.js
+++ b/client/cypress/integration/LegacyTests/mapZoomLatLong.spec.js
@@ -6,7 +6,7 @@ describe('Does the map zoom and adjust to lat/long correctly?', () => {
cy.url().should('include', '#3');
});
it('should change to level 4 when you hit the zoom button', () => {
- cy.get('.mapboxgl-ctrl-icon.mapboxgl-ctrl-zoom-in').click();
+ cy.get('.mapboxgl-ctrl-icon.mapboxgl-ctrl-zoom-in').click({force: true});
cy.url().should('include', '#4');
});
it('should show the correct lat/lng coordinates in the URL',
diff --git a/client/src/components/J40Map.module.scss b/client/src/components/J40Map.module.scss
index 5fa1d9fb..a500ef7c 100644
--- a/client/src/components/J40Map.module.scss
+++ b/client/src/components/J40Map.module.scss
@@ -1,3 +1,4 @@
+@use '../styles/design-system.scss' as *;
@import "./utils.scss";
.j40Popup {
@@ -5,8 +6,8 @@
}
.navigationControl {
- left: 1.25em;
- top: 2.5em;
+ left: .75em;
+ top: units(15);
width: 2.5em;
}
diff --git a/client/src/components/J40Map.tsx b/client/src/components/J40Map.tsx
index f77ad518..ef0cb021 100644
--- a/client/src/components/J40Map.tsx
+++ b/client/src/components/J40Map.tsx
@@ -22,9 +22,10 @@ import {useWindowSize} from 'react-use';
import {useFlags} from '../contexts/FlagContext';
// Components:
-import TerritoryFocusControl from './territoryFocusControl';
-import MapInfoPanel from './mapInfoPanel';
import AreaDetail from './AreaDetail';
+import MapInfoPanel from './mapInfoPanel';
+import MapSearch from './MapSearch';
+import TerritoryFocusControl from './territoryFocusControl';
// Styles and constants
import {makeMapStyle} from '../data/mapStyle';
@@ -52,6 +53,7 @@ export interface IDetailViewInterface {
properties: constants.J40Properties,
};
+
const J40Map = ({location}: IJ40Interface) => {
// Hash portion of URL is of the form #zoom/lat/lng
const [zoom, lat, lng] = location.hash.slice(1).split('/');
@@ -176,6 +178,23 @@ const J40Map = ({location}: IJ40Interface) => {
return (
<>
+
+ {/*
+ The MapSearch component is wrapped in a div in order for MapSearch to render correctly in a production build.
+
+ When the MapSearch component is placed behind a feature flag without a div wrapping
+ MapSearch, the production build will inject CSS due to the null in the false conditional
+ case. Any changes to this (ie, changes to MapSearch or removing feature flag, etc), should
+ be tested with a production build via:
+
+ npm run clean && npm run build && npm run serve
+
+ to ensure the production build works and that MapSearch and the map (ReactMapGL) render correctly.
+ */}
+
+ {'sr' in flags ? : null}
+
+
{
{geolocationInProgress ?
Geolocation in progress...
: ''}
{'fs' in flags ? :'' }
+
diff --git a/client/src/components/MapSearch/MapSearch.module.scss b/client/src/components/MapSearch/MapSearch.module.scss
new file mode 100644
index 00000000..b11b63ec
--- /dev/null
+++ b/client/src/components/MapSearch/MapSearch.module.scss
@@ -0,0 +1,9 @@
+@use '../../styles/design-system.scss' as *;
+
+.mapSearchContainer {
+ position: absolute;
+ top: units(4);
+ left: units(1.5);
+ width: 50%;
+ z-index: 1;
+}
diff --git a/client/src/components/MapSearch/MapSearch.module.scss.d.ts b/client/src/components/MapSearch/MapSearch.module.scss.d.ts
new file mode 100644
index 00000000..3c5d4ae0
--- /dev/null
+++ b/client/src/components/MapSearch/MapSearch.module.scss.d.ts
@@ -0,0 +1,13 @@
+declare namespace MapSearchModuleScssNamespace {
+ export interface IMapSearchModuleScss {
+ mapSearchContainer: string;
+ }
+ }
+
+declare const MapSearchModuleScssModule: MapSearchModuleScssNamespace.IMapSearchModuleScss & {
+ /** WARNING: Only available when `css-loader` is used without `style-loader` or `mini-css-extract-plugin` */
+ locals: MapSearchModuleScssNamespace.IMapSearchModuleScss;
+ };
+
+ export = MapSearchModuleScssModule;
+
diff --git a/client/src/components/MapSearch/MapSearch.test.tsx b/client/src/components/MapSearch/MapSearch.test.tsx
new file mode 100644
index 00000000..dc5d29bc
--- /dev/null
+++ b/client/src/components/MapSearch/MapSearch.test.tsx
@@ -0,0 +1,18 @@
+import * as React from 'react';
+import {render} from '@testing-library/react';
+import {LocalizedComponent} from '../../test/testHelpers';
+import MapSearch from './MapSearch';
+
+describe('rendering of the MapSearch', () => {
+ const mockGoToPlace = jest.fn((x) => x);
+
+ const {asFragment} = render(
+
+
+ ,
+ );
+
+ it('checks if component renders', () => {
+ expect(asFragment()).toMatchSnapshot();
+ });
+});
diff --git a/client/src/components/MapSearch/MapSearch.tsx b/client/src/components/MapSearch/MapSearch.tsx
new file mode 100644
index 00000000..851256a5
--- /dev/null
+++ b/client/src/components/MapSearch/MapSearch.tsx
@@ -0,0 +1,72 @@
+import React, {useState} from 'react';
+import {LngLatBoundsLike} from 'mapbox-gl';
+import {useIntl} from 'gatsby-plugin-intl';
+import {Search} from '@trussworks/react-uswds';
+
+import MapSearchMessage from '../MapSearchMessage';
+
+import * as styles from './MapSearch.module.scss';
+import * as EXPLORE_COPY from '../../data/copy/explore';
+
+interface IMapSearch {
+ goToPlace(bounds: LngLatBoundsLike):void;
+}
+
+const MapSearch = ({goToPlace}:IMapSearch) => {
+ // State to hold if the search results are empty or not:
+ const [isSearchResultsNull, setIsSearchResultsNull] = useState(false);
+ const intl = useIntl();
+
+ /*
+ onSearchHandler will
+ 1. extract the search term from the input field
+ 2. fetch data from the API and return the results as JSON and results to US only
+ 3. if data is valid, destructure the boundingBox values from the search results
+ 4. pan the map to that location
+ */
+ const onSearchHandler = async (event: React.FormEvent) => {
+ event.preventDefault();
+
+ const searchTerm = (event.currentTarget.elements.namedItem('search') as HTMLInputElement).value;
+
+ const searchResults = await fetch(
+ `https://nominatim.openstreetmap.org/search?q=${searchTerm}&format=json&countrycodes=us`,
+ {
+ mode: 'cors',
+ })
+ .then((response) => {
+ if (!response.ok) {
+ throw new Error('Network response was not OK');
+ }
+ return response.json();
+ })
+ .catch((error) => {
+ console.error('There has been a problem with your fetch operation:', error);
+ });
+
+
+ // If results are valid, set isSearchResultsNull to false and pan map to location:
+ if (searchResults && searchResults.length > 0) {
+ setIsSearchResultsNull(false);
+ console.log('Nominatum search results: ', searchResults);
+
+ const [latMin, latMax, longMin, longMax] = searchResults[0].boundingbox;
+ goToPlace([[Number(longMin), Number(latMin)], [Number(longMax), Number(latMax)]]);
+ } else {
+ setIsSearchResultsNull(true);
+ }
+ };
+
+ return (
+
+
+ onSearchHandler(e)}
+ />
+
+ );
+};
+
+export default MapSearch;
diff --git a/client/src/components/MapSearch/__snapshots__/MapSearch.test.tsx.snap b/client/src/components/MapSearch/__snapshots__/MapSearch.test.tsx.snap
new file mode 100644
index 00000000..8fb67c6f
--- /dev/null
+++ b/client/src/components/MapSearch/__snapshots__/MapSearch.test.tsx.snap
@@ -0,0 +1,43 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`rendering of the MapSearch checks if component renders 1`] = `
+
+
+
+ No location found. Please try another location.
+
+
+
+
+`;
diff --git a/client/src/components/MapSearch/index.tsx b/client/src/components/MapSearch/index.tsx
new file mode 100644
index 00000000..3d147e7c
--- /dev/null
+++ b/client/src/components/MapSearch/index.tsx
@@ -0,0 +1,3 @@
+import MapSearch from './MapSearch';
+
+export default MapSearch;
diff --git a/client/src/components/MapSearchMessage/MapSearchMessage.module.scss b/client/src/components/MapSearchMessage/MapSearchMessage.module.scss
new file mode 100644
index 00000000..deb31629
--- /dev/null
+++ b/client/src/components/MapSearchMessage/MapSearchMessage.module.scss
@@ -0,0 +1,18 @@
+@use '../../styles/design-system.scss' as *;
+
+@mixin searchMessageLayout {
+ color: red;
+ background-color: white;
+ @include u-margin-bottom(.5);
+ @include u-padding-left(1);
+}
+
+.showMessage {
+ @include searchMessageLayout;
+ display: block;
+}
+
+.hideMessage {
+ @include searchMessageLayout;
+ visibility: hidden;
+}
\ No newline at end of file
diff --git a/client/src/components/MapSearchMessage/MapSearchMessage.module.scss.d.ts b/client/src/components/MapSearchMessage/MapSearchMessage.module.scss.d.ts
new file mode 100644
index 00000000..bf78bae8
--- /dev/null
+++ b/client/src/components/MapSearchMessage/MapSearchMessage.module.scss.d.ts
@@ -0,0 +1,14 @@
+declare namespace MapSearchMessageModuleScssNamespace {
+ export interface IMapSearchMessageModuleScss {
+ showMessage: string;
+ hideMessage: string;
+ }
+ }
+
+declare const MapSearchMessageModuleScssModule: MapSearchMessageModuleScssNamespace.IMapSearchMessageModuleScss & {
+ /** WARNING: Only available when `css-loader` is used without `style-loader` or `mini-css-extract-plugin` */
+ locals: MapSearchMessageModuleScssNamespace.IMapSearchMessageModuleScss;
+ };
+
+ export = MapSearchMessageModuleScssModule;
+
diff --git a/client/src/components/MapSearchMessage/MapSearchMessage.test.tsx b/client/src/components/MapSearchMessage/MapSearchMessage.test.tsx
new file mode 100644
index 00000000..7c28355b
--- /dev/null
+++ b/client/src/components/MapSearchMessage/MapSearchMessage.test.tsx
@@ -0,0 +1,28 @@
+import * as React from 'react';
+import {render} from '@testing-library/react';
+import {LocalizedComponent} from '../../test/testHelpers';
+import MapSearchMessage from './MapSearchMessage';
+
+describe('rendering of the MapSearchMessage when search results are empty', () => {
+ const {asFragment} = render(
+
+
+ ,
+ );
+
+ it('checks if component renders', () => {
+ expect(asFragment()).toMatchSnapshot();
+ });
+});
+
+describe('rendering of the MapSearchMessage when search results are not empty', () => {
+ const {asFragment} = render(
+
+
+ ,
+ );
+
+ it('checks if component renders', () => {
+ expect(asFragment()).toMatchSnapshot();
+ });
+});
diff --git a/client/src/components/MapSearchMessage/MapSearchMessage.tsx b/client/src/components/MapSearchMessage/MapSearchMessage.tsx
new file mode 100644
index 00000000..802599db
--- /dev/null
+++ b/client/src/components/MapSearchMessage/MapSearchMessage.tsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import {useIntl} from 'gatsby-plugin-intl';
+
+import * as EXPLORE_COPY from '../../data/copy/explore';
+import * as styles from './MapSearchMessage.module.scss';
+
+interface ISearchMessage {
+ isSearchResultsNull: boolean;
+};
+
+const MapSearchMessage = ({isSearchResultsNull}:ISearchMessage) => {
+ const intl = useIntl();
+
+ return (
+