mirror of
https://github.com/DOI-DO/j40-cejst-2.git
synced 2025-07-29 01:31:17 -07:00
[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
This commit is contained in:
parent
84874ee4a5
commit
8e31ca032c
18 changed files with 294 additions and 7 deletions
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
9
client/src/components/MapSearch/MapSearch.module.scss
Normal file
9
client/src/components/MapSearch/MapSearch.module.scss
Normal 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;
|
||||
}
|
13
client/src/components/MapSearch/MapSearch.module.scss.d.ts
vendored
Normal file
13
client/src/components/MapSearch/MapSearch.module.scss.d.ts
vendored
Normal 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;
|
||||
|
18
client/src/components/MapSearch/MapSearch.test.tsx
Normal file
18
client/src/components/MapSearch/MapSearch.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
72
client/src/components/MapSearch/MapSearch.tsx
Normal file
72
client/src/components/MapSearch/MapSearch.tsx
Normal 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;
|
|
@ -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>
|
||||
`;
|
3
client/src/components/MapSearch/index.tsx
Normal file
3
client/src/components/MapSearch/index.tsx
Normal file
|
@ -0,0 +1,3 @@
|
|||
import MapSearch from './MapSearch';
|
||||
|
||||
export default MapSearch;
|
|
@ -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;
|
||||
}
|
14
client/src/components/MapSearchMessage/MapSearchMessage.module.scss.d.ts
vendored
Normal file
14
client/src/components/MapSearchMessage/MapSearchMessage.module.scss.d.ts
vendored
Normal 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;
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
21
client/src/components/MapSearchMessage/MapSearchMessage.tsx
Normal file
21
client/src/components/MapSearchMessage/MapSearchMessage.tsx
Normal 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;
|
|
@ -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 />`;
|
3
client/src/components/MapSearchMessage/index.tsx
Normal file
3
client/src/components/MapSearchMessage/index.tsx
Normal file
|
@ -0,0 +1,3 @@
|
|||
import MapSearchMessage from './MapSearchMessage';
|
||||
|
||||
export default MapSearchMessage;
|
|
@ -5,6 +5,7 @@ import {URLFlagProvider} from '../contexts/FlagContext';
|
|||
|
||||
import J40Header from './J40Header';
|
||||
import J40Footer from './J40Footer';
|
||||
|
||||
interface ILayoutProps {
|
||||
children: ReactNode,
|
||||
location: Location,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue