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 (
+