Skip to content

Commit 7151f08

Browse files
authored
zoom in onRegionChange + MapCircle components (#770)
* Fix zoom not triggering onRegionChange on web * Update delay between for `onRegionChange` * include zoom in `onRegionChange` * `animateToLocation` parse coordinates as numbers if passed as strings * Add MapCircle component * Use `??` instead of `||`
1 parent 5065a3e commit 7151f08

File tree

10 files changed

+132
-39
lines changed

10 files changed

+132
-39
lines changed

packages/core/src/hooks.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ export const useDebounce = <T>(value: T, delay: number): T => {
1818
const [debouncedValue, setDebouncedValue] = React.useState(value);
1919

2020
React.useEffect(() => {
21-
const hanlder = setTimeout(() => {
21+
const handler = setTimeout(() => {
2222
setDebouncedValue(value);
2323
}, delay);
2424

2525
return () => {
26-
clearTimeout(hanlder);
26+
clearTimeout(handler);
2727
};
2828
}, [value, delay]);
2929

packages/maps/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"@draftbit/ui": "48.4.11",
4343
"@react-google-maps/api": "~2.18.1",
4444
"@teovilla/react-native-web-maps": "^0.9.1",
45+
"color": "^4.2.3",
4546
"lodash.isequal": "^4.5.0",
4647
"react-native-maps": "1.3.2"
4748
},
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import * as React from "react";
2+
import { Platform } from "react-native";
3+
import { Circle as MapCircleComponent } from "./react-native-maps";
4+
import type { MapCircleProps as MapCircleComponentProps } from "react-native-maps";
5+
import { withTheme, DefaultTheme } from "@draftbit/ui";
6+
import Color from "color";
7+
8+
export interface MapCircleProps
9+
extends Omit<MapCircleComponentProps, "center"> {
10+
latitude: number;
11+
longitude: number;
12+
theme: typeof DefaultTheme;
13+
}
14+
15+
const MapCircle: React.FC<React.PropsWithChildren<MapCircleProps>> = ({
16+
theme,
17+
latitude,
18+
longitude,
19+
radius = 2000,
20+
fillColor: fillColorProp = theme.colors.primary,
21+
strokeColor = theme.colors.primary,
22+
...rest
23+
}) => {
24+
// Web maps by default uses a lower opacity for the circle, native needs an extra step
25+
const fillColor =
26+
Platform.OS === "web"
27+
? fillColorProp
28+
: Color(fillColorProp).alpha(0.3).rgb().string();
29+
30+
return (
31+
<MapCircleComponent
32+
center={{
33+
latitude,
34+
longitude,
35+
}}
36+
radius={radius}
37+
fillColor={fillColor}
38+
strokeColor={strokeColor}
39+
{...rest}
40+
/>
41+
);
42+
};
43+
44+
export default withTheme(MapCircle);

packages/maps/src/components/MapView.tsx

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import { MapViewContext, ZoomLocation } from "./MapViewCommon";
1212
import { MapMarkerClusterView } from "./marker-cluster";
1313
import { flattenReactFragments } from "@draftbit/ui";
1414
import type { MapMarker as MapMarkerRefType } from "react-native-maps";
15-
import { useDeepCompareMemo } from "./useDeepCompareMemo";
15+
import { useDeepCompareMemo, useDebounce } from "../utils";
16+
import MapCircle from "./MapCircle";
1617

1718
export interface MapMarkerContextType {
1819
onMarkerPress: (marker: MapMarkerProps) => void;
@@ -26,8 +27,15 @@ export const MapMarkerContext = React.createContext<MapMarkerContextType>({
2627
getMarkerRef: () => undefined,
2728
});
2829

30+
interface RegionWithZoom extends Region {
31+
zoom: number;
32+
}
33+
2934
export interface MapViewProps<T>
30-
extends Omit<MapViewComponentProps, "onRegionChangeComplete" | "onPress"> {
35+
extends Omit<
36+
MapViewComponentProps,
37+
"onRegionChangeComplete" | "onPress" | "onRegionChange"
38+
> {
3139
apiKey: string;
3240
zoom?: number;
3341
latitude?: number;
@@ -37,7 +45,7 @@ export interface MapViewProps<T>
3745
markersData?: T[];
3846
keyExtractor?: (item: T, index: number) => string;
3947
renderItem?: ({ item, index }: { item: T; index: number }) => JSX.Element;
40-
onRegionChange?: (region: Region) => void;
48+
onRegionChange?: (region: RegionWithZoom) => void;
4149
onPress?: (latitude: number, longitude: number) => void;
4250
}
4351

@@ -66,6 +74,7 @@ const MapViewF = <T extends object>({
6674
mapRef: React.RefObject<MapViewComponent>;
6775
}) => {
6876
const [currentRegion, setCurrentRegion] = React.useState<Region | null>(null);
77+
const delayedRegionValue = useDebounce(currentRegion, 300);
6978

7079
const markerRefs = React.useMemo<
7180
Map<string, React.RefObject<MapMarkerRefType>>
@@ -216,10 +225,28 @@ const MapViewF = <T extends object>({
216225
}
217226
}, [latitude, longitude, zoom, animateToLocation]);
218227

228+
// Use delayed/debounced value to prevent too many calls when map is being dragged
229+
React.useEffect(() => {
230+
const callOnRegionChange = async () => {
231+
if (delayedRegionValue) {
232+
const camera = await mapRef.current?.getCamera();
233+
onRegionChange?.({ ...delayedRegionValue, zoom: camera?.zoom ?? 1 });
234+
}
235+
};
236+
237+
callOnRegionChange();
238+
// onRegionChange excluded to prevent calling on every rerender when using an anonymous function (which is most common)
239+
// eslint-disable-next-line react-hooks/exhaustive-deps
240+
}, [delayedRegionValue]);
241+
219242
const markers = React.useMemo(
220243
() => getChildrenForType(MapMarker),
221244
[getChildrenForType]
222245
);
246+
const circles = React.useMemo(
247+
() => getChildrenForType(MapCircle),
248+
[getChildrenForType]
249+
);
223250
const clusters = React.useMemo(
224251
() => getChildrenForType(MapMarkerCluster),
225252
[getChildrenForType]
@@ -255,9 +282,6 @@ const MapViewF = <T extends object>({
255282
showsCompass={showsCompass}
256283
initialCamera={camera}
257284
loadingEnabled={loadingEnabled}
258-
onRegionChangeComplete={(region) => {
259-
onRegionChange?.(region);
260-
}}
261285
onRegionChange={setCurrentRegion}
262286
onPress={(event) => {
263287
const coordinate = event.nativeEvent.coordinate;
@@ -290,6 +314,8 @@ const MapViewF = <T extends object>({
290314
<React.Fragment key={index}>{cluster}</React.Fragment>
291315
))}
292316
</MapMarkerContext.Provider>
317+
318+
{circles}
293319
</MapViewComponent>
294320
),
295321
[
@@ -337,8 +363,8 @@ class MapView<T extends object> extends React.Component<
337363
heading: 0,
338364
pitch: 0,
339365
center: {
340-
latitude,
341-
longitude,
366+
latitude: Number(latitude),
367+
longitude: Number(longitude),
342368
},
343369
};
344370

Lines changed: 31 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from "react";
22
import { ViewStyle, StyleProp, Text, View } from "react-native";
33
import { MapMarkerClusterContext } from "./MapMarkerClusterContext";
4-
import { withTheme } from "@draftbit/ui";
4+
import { withTheme, DefaultTheme } from "@draftbit/ui";
55

66
interface MapMarkerClusterViewProps {
77
zoomOnPress?: boolean;
@@ -27,36 +27,38 @@ const MapMarkerClusterView: React.FC<MapMarkerClusterViewProps> = ({
2727
);
2828
};
2929

30-
export const DefaultMapMarkerClusterView = withTheme(({ theme }) => {
31-
return (
32-
<MapMarkerClusterView
33-
renderItem={({ markerCount }) => (
34-
<View
35-
testID="default-map-marker-cluster-view"
36-
style={{
37-
backgroundColor: theme.colors.primary,
38-
borderColor: theme.colors.background,
39-
borderWidth: 1,
40-
borderRadius: 15,
41-
paddingHorizontal: 3,
42-
minWidth: 30,
43-
minHeight: 30,
44-
alignItems: "center",
45-
justifyContent: "center",
46-
}}
47-
>
48-
<Text
30+
export const DefaultMapMarkerClusterView = withTheme(
31+
({ theme }: { theme: typeof DefaultTheme }) => {
32+
return (
33+
<MapMarkerClusterView
34+
renderItem={({ markerCount }) => (
35+
<View
36+
testID="default-map-marker-cluster-view"
4937
style={{
50-
color: theme.colors.background,
51-
textAlign: "center",
38+
backgroundColor: theme.colors.primary,
39+
borderColor: theme.colors.background,
40+
borderWidth: 1,
41+
borderRadius: 15,
42+
paddingHorizontal: 3,
43+
minWidth: 30,
44+
minHeight: 30,
45+
alignItems: "center",
46+
justifyContent: "center",
5247
}}
5348
>
54-
{markerCount}
55-
</Text>
56-
</View>
57-
)}
58-
/>
59-
);
60-
});
49+
<Text
50+
style={{
51+
color: theme.colors.background,
52+
textAlign: "center",
53+
}}
54+
>
55+
{markerCount}
56+
</Text>
57+
</View>
58+
)}
59+
/>
60+
);
61+
}
62+
);
6163

6264
export default MapMarkerClusterView;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { Circle } from "react-native-maps";
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { Circle } from "@teovilla/react-native-web-maps";
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { default as default } from "./MapView";
22
export { Callout } from "./Callout";
33
export { Marker } from "./Marker";
4+
export { Circle } from "./Circle";

packages/maps/src/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export {
55
MapMarkerClusterView,
66
} from "./components/marker-cluster";
77
export { default as MapCallout } from "./components/MapCallout";
8+
export { default as MapCircle } from "./components/MapCircle";

packages/maps/src/components/useDeepCompareMemo.ts renamed to packages/maps/src/utils.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,19 @@ export function useDeepCompareMemo<T>(
2121
// eslint-disable-next-line react-hooks/exhaustive-deps
2222
return React.useMemo(factory, deps?.map(useDeepCompareMemoize));
2323
}
24+
25+
export function useDebounce<T>(value: T, delay: number): T {
26+
const [debouncedValue, setDebouncedValue] = React.useState(value);
27+
28+
React.useEffect(() => {
29+
const handler = setTimeout(() => {
30+
setDebouncedValue(value);
31+
}, delay);
32+
33+
return () => {
34+
clearTimeout(handler);
35+
};
36+
}, [value, delay]);
37+
38+
return debouncedValue;
39+
}

0 commit comments

Comments
 (0)