Zoom button styling does not match spec // General custom controls fix (#357)

This commit is contained in:
Nat Hillard 2021-07-15 10:28:51 -04:00 committed by GitHub
commit dbf1ae2ad8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 31006 additions and 294 deletions

View file

@ -1,131 +1,132 @@
/* eslint-disable no-unused-vars */
import React, {useRef, useEffect, useState} from 'react';
import maplibregl, {LngLatBoundsLike,
Map,
import React, {MouseEvent, useRef, useState} from 'react';
import {Map, MapboxGeoJSONFeature, LngLatBoundsLike} from 'maplibre-gl';
import ReactMapGL, {
MapEvent,
ViewportProps,
WebMercatorViewport,
NavigationControl,
PopupOptions,
GeolocateControl,
Popup,
LngLatLike,
MapboxGeoJSONFeature} from 'maplibre-gl';
FlyToInterpolator,
FullscreenControl,
MapRef} from 'react-map-gl';
import {makeMapStyle} from '../data/mapStyle';
import PopupContent from './popupContent';
import * as constants from '../data/constants';
import ReactDOM from 'react-dom';
import AreaDetail from './areaDetail';
import bbox from '@turf/bbox';
import * as d3 from 'd3-ease';
import {useFlags} from '../contexts/FlagContext';
import TerritoryFocusControl from './territoryFocusControl';
import 'maplibre-gl/dist/maplibre-gl.css';
import * as constants from '../data/constants';
import * as styles from './J40Map.module.scss';
declare global {
interface Window {
Cypress?: object;
underlyingMap: Map;
}
}
type ClickEvent = maplibregl.MapMouseEvent & maplibregl.EventData;
interface IDetailViewInterface {
latitude: number
longitude: number
zoom: number
properties: constants.J40Properties,
};
const J40Map = () => {
const mapContainer = React.useRef<HTMLDivElement>(null);
const mapRef = useRef<Map>() as React.MutableRefObject<Map>;
const selectedFeature = useRef<MapboxGeoJSONFeature>();
const [zoom, setZoom] = useState(constants.GLOBAL_MIN_ZOOM);
const [viewport, setViewport] = useState<ViewportProps>({
latitude: constants.DEFAULT_CENTER[0],
longitude: constants.DEFAULT_CENTER[1],
zoom: constants.GLOBAL_MIN_ZOOM,
});
const [selectedFeature, setSelectedFeature] = useState<MapboxGeoJSONFeature>();
const [detailViewData, setDetailViewData] = useState<IDetailViewInterface>();
const [transitionInProgress, setTransitionInProgress] = useState<boolean>(false);
const [geolocationInProgress, setGeolocationInProgress] = useState<boolean>(false);
const mapRef = useRef<MapRef>(null);
const flags = useFlags();
useEffect(() => {
const initialMap = new Map({
container: mapContainer.current!,
style: makeMapStyle(flags),
center: constants.DEFAULT_CENTER as LngLatLike,
zoom: zoom,
minZoom: constants.GLOBAL_MIN_ZOOM,
maxBounds: constants.GLOBAL_MAX_BOUNDS as LngLatBoundsLike,
hash: true, // Adds hash of zoom/lat/long to the url
});
// disable map rotation using right click + drag
initialMap.dragRotate.disable();
// disable map rotation using touch rotation gesture
initialMap.touchZoomRotate.disableRotation();
setZoom(initialMap.getZoom());
initialMap.on('load', () => {
if (window.Cypress) {
window.underlyingMap = initialMap;
const onClick = (event: MapEvent) => {
const feature = event.features && event.features[0];
if (feature) {
const [minLng, minLat, maxLng, maxLat] = bbox(feature);
const newViewPort = new WebMercatorViewport({height: viewport.height!, width: viewport.width!});
const {longitude, latitude, zoom} = newViewPort.fitBounds(
[
[minLng, minLat],
[maxLng, maxLat],
],
{
padding: 40,
},
);
// If we've selected a new feature, set 'selected' to false
if (selectedFeature && feature.id !== selectedFeature.id) {
setMapSelected(selectedFeature, false);
}
});
setMapSelected(feature, true);
const popupInfo = {
longitude: longitude,
latitude: latitude,
zoom: zoom,
properties: feature.properties,
};
goToPlace([
[minLng, minLat],
[maxLng, maxLat],
]);
setDetailViewData(popupInfo);
}
};
const onLoad = () => {
if (typeof window !== 'undefined' && window.Cypress && mapRef.current) {
window.underlyingMap = mapRef.current.getMap();
}
};
const goToPlace = (bounds: LngLatBoundsLike ) => {
const {longitude, latitude, zoom} = new WebMercatorViewport({height: viewport.height!, width: viewport.width!})
.fitBounds(bounds as [[number, number], [number, number]], {
padding: 20,
offset: [0, -100],
});
setViewport({
...viewport,
longitude,
latitude,
zoom,
transitionDuration: 1000,
transitionInterpolator: new FlyToInterpolator(),
transitionEasing: d3.easeCubic,
});
};
initialMap.on('click', handleClick);
initialMap.addControl(new NavigationControl({showCompass: false}), 'top-left');
mapRef.current = initialMap;
}, []);
const setMapSelected = (feature:MapboxGeoJSONFeature, isSelected:boolean) : void => {
// The below can be confirmed during debug with:
// mapRef.current.getFeatureState({"id":feature.id, "source":feature.source, "sourceLayer":feature.sourceLayer})
mapRef.current.setFeatureState({
mapRef.current && mapRef.current.getMap().setFeatureState({
source: feature.source,
sourceLayer: feature.sourceLayer,
id: feature.id,
}, {[constants.SELECTED_PROPERTY]: isSelected});
if (isSelected) {
selectedFeature.current = feature;
setSelectedFeature(feature);
} else {
selectedFeature.current = undefined;
setSelectedFeature(undefined);
}
};
const handleClick = (e: ClickEvent) => {
const map = e.target;
const clickedCoord = e.point;
const features = map.queryRenderedFeatures(clickedCoord, {
layers: [constants.HIGH_SCORE_LAYER_NAME],
});
const feature = features && features[0];
if (feature) {
const placeholder = document.createElement('div');
// If we've selected a new feature, set 'selected' to false
if (selectedFeature.current && feature.id !== selectedFeature.current.id) {
setMapSelected(selectedFeature.current, false);
}
setMapSelected(feature, true);
ReactDOM.render(<PopupContent properties={feature.properties} />, placeholder);
const options : PopupOptions = {
offset: [0, 0],
className: styles.j40Popup,
focusAfterOpen: false,
};
new Popup(options)
.setLngLat(e.lngLat)
.setDOMContent(placeholder)
.on('close', function() {
setMapSelected(feature, false);
})
.addTo(map);
}
};
useEffect(() => {
mapRef.current.on('move', () => {
setZoom(mapRef.current.getZoom());
});
mapRef.current.on('mouseenter', constants.HIGH_SCORE_LAYER_NAME, () => {
mapRef.current.getCanvas().style.cursor = 'pointer';
});
mapRef.current.on('mouseleave', constants.HIGH_SCORE_LAYER_NAME, () => {
mapRef.current.getCanvas().style.cursor = '';
});
}, [mapRef]);
const goToPlace = (bounds:number[][]) => {
mapRef.current.fitBounds(bounds as LngLatBoundsLike);
};
const onClickTerritoryFocusButton = (event: React.MouseEvent<HTMLButtonElement>) => {
// currentTarget always refers to the element to which the event handler
// has been attached, as opposed to Event.target, which identifies
// the element on which the event occurred and which may be its descendant.
const buttonID = event.target && event.currentTarget.id;
const onClickTerritoryFocusButton = (event: MouseEvent<HTMLButtonElement>) => {
const buttonID = event.target && (event.target as HTMLElement).id;
switch (buttonID) {
case '48':
@ -146,40 +147,74 @@ const J40Map = () => {
}
};
const onTransitionStart = () => {
setTransitionInProgress(true);
};
const onTransitionEnd = () => {
setTransitionInProgress(false);
};
const onGeolocate = () => {
setGeolocationInProgress(false);
};
const onClickGeolocate = () => {
setGeolocationInProgress(true);
};
return (
<div>
<div className={styles.territoryFocusContainer}>
<button
id={'48'}
onClick={onClickTerritoryFocusButton}
className={styles.territoryFocusButton}
aria-label="Zoom to Lower 48" >
48
</button>
<button
id={'AK'}
onClick={onClickTerritoryFocusButton}
className={styles.territoryFocusButton}
aria-label="Zoom to Alaska" >
AK
</button>
<button
id={'HI'}
onClick={onClickTerritoryFocusButton}
className={styles.territoryFocusButton}
aria-label="Zoom to Hawaii" >
HI
</button>
<button
id={'PR'}
onClick={onClickTerritoryFocusButton}
className={styles.territoryFocusButton}
aria-label="Zoom to Puerto Rico" >
PR
</button>
</div>
<div ref={mapContainer} className={styles.mapContainer}/>
</div>
<>
<ReactMapGL
{...viewport}
className={styles.mapContainer}
mapStyle={makeMapStyle(flags)}
minZoom={constants.GLOBAL_MIN_ZOOM}
maxZoom={constants.GLOBAL_MAX_ZOOM}
mapOptions={{hash: true}}
width="100%"
height="52vw"
dragRotate={false}
touchRotate={false}
interactiveLayerIds={[constants.HIGH_SCORE_LAYER_NAME]}
onViewportChange={setViewport}
onClick={onClick}
onLoad={onLoad}
onTransitionStart={onTransitionStart}
onTransitionEnd={onTransitionEnd}
ref={mapRef}
>
{(detailViewData && !transitionInProgress) && (
<Popup
className={styles.j40Popup}
tipSize={5}
anchor="top"
longitude={detailViewData.longitude!}
latitude={detailViewData.latitude!}
closeOnClick={true}
onClose={setDetailViewData}
captureScroll={true}
>
<AreaDetail properties={detailViewData.properties} />
</Popup>
)}
<NavigationControl
showCompass={false}
className={styles.navigationControl}
/>
{'gl' in flags ? <GeolocateControl
className={styles.geolocateControl}
positionOptions={{enableHighAccuracy: true}}
onGeolocate={onGeolocate}
// @ts-ignore // Types have not caught up yet, see https://github.com/visgl/react-map-gl/issues/1492
onClick={onClickGeolocate}
/> : ''}
{geolocationInProgress ? <div>Geolocation in progress...</div> : ''}
<TerritoryFocusControl onClickTerritoryFocusButton={onClickTerritoryFocusButton}/>
{'fs' in flags ? <FullscreenControl className={styles.fullscreenControl}/> :'' }
</ReactMapGL>
</>
);
};