Merge branch 'usds:main' into main

This commit is contained in:
Saran Ahluwalia 2021-12-03 12:29:13 -05:00 committed by GitHub
commit a64302466d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 302 additions and 7 deletions

View file

@ -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',

View file

@ -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;
}

View file

@ -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 (
<>
<Grid col={12} desktop={{col: 9}}>
{/*
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.
*/}
<div>
{'sr' in flags ? <MapSearch goToPlace={goToPlace}/> : null}
</div>
<ReactMapGL
{...viewport}
mapStyle={makeMapStyle(flags)}
@ -256,6 +275,7 @@ const J40Map = ({location}: IJ40Interface) => {
{geolocationInProgress ? <div>Geolocation in progress...</div> : ''}
<TerritoryFocusControl onClickTerritoryFocusButton={onClickTerritoryFocusButton}/>
{'fs' in flags ? <FullscreenControl className={styles.fullscreenControl}/> :'' }
</ReactMapGL>
</Grid>

View file

@ -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;
}

View file

@ -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;

View file

@ -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(
<LocalizedComponent>
<MapSearch goToPlace={mockGoToPlace}/>
</LocalizedComponent>,
);
it('checks if component renders', () => {
expect(asFragment()).toMatchSnapshot();
});
});

View file

@ -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<HTMLFormElement>) => {
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 (
<div className={styles.mapSearchContainer}>
<MapSearchMessage isSearchResultsNull={isSearchResultsNull} />
<Search
placeholder={intl.formatMessage(EXPLORE_COPY.MAP.SEARCH_PLACEHOLDER)}
size="small"
onSubmit={(e) => onSearchHandler(e)}
/>
</div>
);
};
export default MapSearch;

View file

@ -0,0 +1,43 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`rendering of the MapSearch checks if component renders 1`] = `
<DocumentFragment>
<div>
<div>
No location found. Please try another location.
</div>
<form
class="usa-search usa-search--small"
data-testid="form"
role="search"
>
<label
class="usa-sr-only"
data-testid="label"
for="search-field"
>
Search
</label>
<input
class="usa-input"
data-testid="textInput"
id="search-field"
name="search"
placeholder="Enter a city, state or ZIP"
type="search"
/>
<button
class="usa-button"
data-testid="button"
type="submit"
>
<span
class="usa-sr-only"
>
Search
</span>
</button>
</form>
</div>
</DocumentFragment>
`;

View file

@ -0,0 +1,3 @@
import MapSearch from './MapSearch';
export default MapSearch;

View file

@ -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;
}

View file

@ -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;

View file

@ -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(
<LocalizedComponent>
<MapSearchMessage isSearchEmpty={true}/>
</LocalizedComponent>,
);
it('checks if component renders', () => {
expect(asFragment()).toMatchSnapshot();
});
});
describe('rendering of the MapSearchMessage when search results are not empty', () => {
const {asFragment} = render(
<LocalizedComponent>
<MapSearchMessage isSearchEmpty={false}/>
</LocalizedComponent>,
);
it('checks if component renders', () => {
expect(asFragment()).toMatchSnapshot();
});
});

View file

@ -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 (
<div className={isSearchResultsNull ? styles.showMessage : styles.hideMessage}>
{intl.formatMessage(EXPLORE_COPY.MAP.SEARCH_RESULTS_EMPTY_MESSAGE)}
</div>
);
};
export default MapSearchMessage;

View file

@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`rendering of the MapSearchMessage when search results are empty checks if component renders 1`] = `
<DocumentFragment>
<div>
No location found. Please try another location.
</div>
</DocumentFragment>
`;
exports[`rendering of the MapSearchMessage when search results are not empty checks if component renders 1`] = `<DocumentFragment />`;

View file

@ -0,0 +1,3 @@
import MapSearchMessage from './MapSearchMessage';
export default MapSearchMessage;

View file

@ -5,6 +5,7 @@ import {URLFlagProvider} from '../contexts/FlagContext';
import J40Header from './J40Header';
import J40Footer from './J40Footer';
interface ILayoutProps {
children: ReactNode,
location: Location,

View file

@ -1,8 +1,10 @@
@use '../styles/design-system.scss' as *;
.territoryFocusContainer {
display: flex;
flex-direction: column;
position: absolute;
left: 20px;
top: 150px;
left: .75em;
top: units(card-lg);
z-index: 10;
}

View file

@ -97,6 +97,16 @@ export const MAP = defineMessages({
defaultMessage: 'Puerto Rico',
description: 'The full name indicating the bounds of Puerto Rico',
},
SEARCH_PLACEHOLDER: {
id: 'map.search.placeholder.text',
defaultMessage: 'Enter a city, state or ZIP',
description: 'placeholder text for search',
},
SEARCH_RESULTS_EMPTY_MESSAGE: {
id: 'map.search.results.empty.text',
defaultMessage: 'No location found. Please try another location.',
description: 'text displaying message for no search results found',
},
});
// Side Panel copy

View file

@ -15,6 +15,12 @@ class GeoCorrETL(ExtractTransformLoad):
self.OUTPUT_PATH = self.DATA_PATH / "dataset" / "geocorr"
# Need to change hyperlink to S3
# Note, that this CSV was generated by this notebook:
# https://github.com/usds/justice40-tool/blob/main/data/data-pipeline/data_pipeline/ipython/urban_vs_rural.ipynb
# The source data for this notebook was downloaded from GeoCorr;
# the instructions for generating the source data is here:
# https://github.com/usds/justice40-tool/issues/355#issuecomment-920241787
self.GEOCORR_PLACES_URL = "https://justice40-data.s3.amazonaws.com/data-sources/geocorr_urban_rural.csv.zip"
self.GEOCORR_GEOID_FIELD_NAME = "GEOID10_TRACT"
self.URBAN_HEURISTIC_FIELD_NAME = "Urban Heuristic Flag"

View file

@ -100,6 +100,8 @@
"metadata": {},
"outputs": [],
"source": [
"# CSV was manually generated\n",
"# Instructions for how to generate the CSV from Geocorr are here: https://github.com/usds/justice40-tool/issues/355#issuecomment-920241787\n",
"geocorr_urban_rural_map = pd.read_csv(\n",
" os.path.join(GEOCORR_DATA_DIR, \"geocorr2014_2125804280.csv\"),\n",
" encoding=\"ISO-8859-1\",\n",