Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented draggable and animated Marker and Annotation #37

Closed
wants to merge 10 commits into from
37 changes: 30 additions & 7 deletions src/components/Annotation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
import { createPortal } from 'react-dom';
import MapContext from '../context/MapContext';
import AnnotationProps from './AnnotationProps';
import { forwardMapkitEvent } from './Map';

export default function Annotation({
latitude,
Expand All @@ -19,6 +20,14 @@ export default function Annotation({
selected = undefined,
onSelect = undefined,
onDeselect = undefined,
onDragStart = undefined,
onDragEnd = undefined,

animates = undefined,
appearanceAnimation = '',
draggable = undefined,
enabled = undefined,

children,
}: AnnotationProps) {
const [annotation, setAnnotation] = useState<mapkit.Annotation | null>(null);
Expand Down Expand Up @@ -48,6 +57,10 @@ export default function Annotation({
accessibilityLabel,

selected,
animates,
appearanceAnimation,
draggable,
enabled,
};
Object.entries(properties).forEach(([propertyName, prop]) => {
useEffect(() => {
Expand All @@ -61,16 +74,26 @@ export default function Annotation({
const events = [
{ name: 'select', handler: onSelect },
{ name: 'deselect', handler: onDeselect },
{ name: 'drag-start', handler: onDragStart },
{ name: 'drag-end', handler: onDragEnd },
] as const;
events.forEach(({ name, handler }) => {
useEffect(() => {
if (!annotation || !handler) return undefined;

const handlerWithoutParameters = () => handler();
type DOMEventTarget = {
coordinate: mapkit.Coordinate,
data: Object
};

type MapKitMapInteractionEvent = {
target: DOMEventTarget
};

annotation.addEventListener(name, handlerWithoutParameters);
return () => annotation.removeEventListener(name, handlerWithoutParameters);
}, [annotation, handler]);
const interactionEvent = ({ target }: MapKitMapInteractionEvent) => ({
coordinate: target.coordinate,
data: target.data,
});

events.forEach(({ name, handler }) => {
forwardMapkitEvent(annotation, name, handler, interactionEvent);
});

return createPortal(children, contentEl);
Expand Down
36 changes: 36 additions & 0 deletions src/components/AnnotationProps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,42 @@ export default interface AnnotationProps {
*/
onDeselect?: () => void;

/**
* Event fired when the user starts a drag for the annotation.
*/
onDragStart?: () => void;

/**
* Event fired when the user ends a drag for the annotation.
*/
onDragEnd?: () => void;

/**
* A Boolean value that determines whether the map animates the annotation.
* @see {@link https://developer.apple.com/documentation/mapkitjs/mapkit/annotation/2973817-animates}
*/
animates?: boolean;

/**
* A CSS animation that runs when the annotation appears on the map.
* @see {@link https://developer.apple.com/documentation/mapkitjs/mapkit/annotation/2973818-appearanceanimation}
*/
appearanceAnimation?: string;

/**
* A Boolean value that determines whether the user can drag the annotation.
*
* (Annotation needs to be enabled in order to be draggable.)
* @see {@link https://developer.apple.com/documentation/mapkitjs/mapkit/annotation/2973826-draggable}
*/
draggable?: boolean;

/**
* A Boolean value that determines whether the annotation responds to user interaction.
* @see {@link https://developer.apple.com/documentation/mapkitjs/mapkit/annotation/2973828-enabled}
*/
enabled?: boolean;

/**
* React children to render inside the annotation.
*/
Expand Down
14 changes: 7 additions & 7 deletions src/components/Map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,32 +14,32 @@ import MapProps from './MapProps';

/**
* Forwards a given MapKit JS event to a mapkit-react event.
* @param map The current map instance
* @param map The current map instance or annotation / marker element
* @param name The name of the MapKit JS event.
* @param handler The event handler of the mapkit-react Map component
* @param eventMap A function that transforms the parameter of the
* MapKit JS event handler to a parameter for the
* mapkit-react event handler.
*/
function forwardMapkitEvent<E>(
map: mapkit.Map | null,
export function forwardMapkitEvent<E>(
element: mapkit.Map | mapkit.Annotation | null,
name: String,
handler: ((mapkitReactEvent: E) => void) | undefined,
eventMap: (mapkitEvent: any) => E,
) {
useEffect(() => {
if (!map || !handler) return undefined;
if (!element || !handler) return undefined;

// @ts-ignore
const mapkitHandler = (e) => {
handler(eventMap(e));
};

// @ts-ignore
map.addEventListener(name, mapkitHandler);
element.addEventListener(name, mapkitHandler);
// @ts-ignore
return () => map.removeEventListener(name, mapkitHandler);
}, [map, handler]);
return () => element.removeEventListener(name, mapkitHandler);
}, [element, handler]);
}

const Map = React.forwardRef<mapkit.Map | null, React.PropsWithChildren<MapProps>>(({
Expand Down
35 changes: 28 additions & 7 deletions src/components/Marker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useContext, useEffect, useState } from 'react';
import MapContext from '../context/MapContext';
import { FeatureVisibility, toMapKitFeatureVisibility } from '../util/parameters';
import MarkerProps from './MarkerProps';
import { forwardMapkitEvent } from './Map';

export default function Marker({
latitude,
Expand All @@ -21,8 +22,14 @@ export default function Marker({
selectedGlyphImage = undefined,

selected = undefined,
animates = undefined,
appearanceAnimation = '',
draggable = undefined,
enabled = undefined,
onSelect = undefined,
onDeselect = undefined,
onDragStart = undefined,
onDragEnd = undefined,
}: MarkerProps) {
const [marker, setMarker] = useState<mapkit.MarkerAnnotation | null>(null);
const map = useContext(MapContext);
Expand Down Expand Up @@ -66,6 +73,10 @@ export default function Marker({
selectedGlyphImage,
clusteringIdentifier,
selected,
animates,
appearanceAnimation,
draggable,
enabled,
};
Object.entries(properties).forEach(([propertyName, prop]) => {
useEffect(() => {
Expand All @@ -79,16 +90,26 @@ export default function Marker({
const events = [
{ name: 'select', handler: onSelect },
{ name: 'deselect', handler: onDeselect },
{ name: 'drag-start', handler: onDragStart },
{ name: 'drag-end', handler: onDragEnd },
] as const;
events.forEach(({ name, handler }) => {
useEffect(() => {
if (!marker || !handler) return undefined;

const handlerWithoutParameters = () => handler();
type DOMEventTarget = {
coordinate: mapkit.Coordinate,
data: Object
};

type MapKitMapInteractionEvent = {
target: DOMEventTarget
};

const interactionEvent = ({ target }: MapKitMapInteractionEvent) => ({
coordinate: target.coordinate,
data: target.data,
});

marker.addEventListener(name, handlerWithoutParameters);
return () => marker.removeEventListener(name, handlerWithoutParameters);
}, [marker, handler]);
events.forEach(({ name, handler }) => {
forwardMapkitEvent(marker, name, handler, interactionEvent);
});

return null;
Expand Down
38 changes: 37 additions & 1 deletion src/components/MarkerProps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,36 @@ export default interface MarkerProps {
selectedGlyphImage?: object | undefined;

/**
* A Boolean value that determines whether the map displays the annotation in a selected state.
* A Boolean value that determines whether the map displays the marker in a selected state.
*/
selected?: boolean;

/**
* A Boolean value that determines whether the map animates the marker.
* @see {@link https://developer.apple.com/documentation/mapkitjs/mapkit/annotation/2973817-animates}
*/
animates?: boolean;

/**
* A CSS animation that runs when the marker appears on the map.
* @see {@link https://developer.apple.com/documentation/mapkitjs/mapkit/annotation/2973818-appearanceanimation}
*/
appearanceAnimation?: string;

/**
* A Boolean value that determines whether the user can drag the marker.
*
* (Marker needs to be enabled in order to be draggable.)
* @see {@link https://developer.apple.com/documentation/mapkitjs/mapkit/annotation/2973817-animates}
*/
draggable?: boolean;

/**
* A Boolean value that determines whether the annotation responds to user interaction.
* @see {@link https://developer.apple.com/documentation/mapkitjs/mapkit/annotation/2973828-enabled}
*/
enabled?: boolean;

/**
* Event fired when the marker is selected.
*/
Expand All @@ -86,6 +112,16 @@ export default interface MarkerProps {
*/
onDeselect?: () => void;

/**
* Event fired with the user initiates a drag for the annotation.
*/
onDragStart?: () => void;

/**
* Event fired with the user ends a drag for the annotation.
*/
onDragEnd?: () => void;

/**
* A shared identifier for all of the member annotations.
* An annotation needs a clusteringIdentifier to be part of an annotation cluster.
Expand Down
36 changes: 36 additions & 0 deletions src/stories/Annotation.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,39 @@ const Template: StoryFn<MarkerProps> = (args) => {

export const Default = Template.bind({});
Default.args = { latitude: 46.52, longitude: 6.57 };

export const MoveableAnnotation = Template.bind({});
MoveableAnnotation.args = {
latitude: 46.52,
longitude: 6.57,
title: 'Tap and hold to move',
draggable: true,
enabled: true,
};

MoveableAnnotation.storyName = 'Moveable Annotation';

export const AnimatedAnnotation = () => {
const initialRegion: CoordinateRegion = useMemo(() => ({
centerLatitude: 46.20738751546706,
centerLongitude: 6.155891756231,
latitudeDelta: 0.007,
longitudeDelta: 0.015,
}), []);

return (
<Map token={token} initialRegion={initialRegion} paddingBottom={44}>
<Annotation
latitude={46.20738751546706}
longitude={6.155891756231}
title="Tap and hold to move"
animates
appearanceAnimation="gelatine 0.5s infinite"
>
<CustomMarker />
</Annotation>
</Map>
);
};

AnimatedAnnotation.storyName = 'Animated Annotation';
21 changes: 21 additions & 0 deletions src/stories/stories.css
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,24 @@ body {
flex: 1;
min-width: 150px;
}

/* Animation FROM https: //codepen.io/nelledejones/pen/gOOPWrK */
@keyframes gelatine {

from,
to {
transform: scale(1, 1);
}

25% {
transform: scale(0.9, 1.1);
}

50% {
transform: scale(1.1, 0.9);
}

75% {
transform: scale(0.95, 1.05);
}
}
Loading