Skip to content

Commit

Permalink
Implement custom callout (#55)
Browse files Browse the repository at this point in the history
* Implement draggable option

* appearanceAnimation implemented with story

* Implemented drag events

* Implement size and padding

* Fix padding and implement anchorOffset

* First implementation of callouts

* Working but ugly example without JSX

* React 17 sample implementation

* Minor Code modifications

* Working implementation

* Annotation implementation

* Storybook examples

* Apply suggestions from code review

Co-authored-by: Nicolas Ettlin <[email protected]>

* Remove ForAnnotation from prop name

* Minor adaptions from Review

* Implement Annotation Storybook example

* Implement display priority conversion

* Remove extended callout descriptions

---------

Co-authored-by: Nicolas Ettlin <[email protected]>
  • Loading branch information
nikischin and Nicolapps authored Apr 27, 2024
1 parent b3cff89 commit 2815a52
Show file tree
Hide file tree
Showing 10 changed files with 707 additions and 23 deletions.
144 changes: 143 additions & 1 deletion src/components/Annotation.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import {
import React, {
useContext,
useEffect,
useState,
useMemo,
useRef,
} from 'react';
import { createPortal } from 'react-dom';
import MapContext from '../context/MapContext';
import AnnotationProps from './AnnotationProps';
import forwardMapkitEvent from '../util/forwardMapkitEvent';
import CalloutContainer from './CalloutContainer';
import { toMapKitDisplayPriority } from '../util/parameters';

export default function Annotation({
latitude,
Expand Down Expand Up @@ -39,6 +42,18 @@ export default function Annotation({
visible = true,

clusteringIdentifier = null,
displayPriority = undefined,
collisionMode = undefined,

calloutElement = undefined,
calloutContent = undefined,
calloutLeftAccessory = undefined,
calloutRightAccessory = undefined,

calloutEnabled = undefined,
calloutOffsetX = 0,
calloutOffsetY = 0,

draggable = false,
enabled = true,

Expand Down Expand Up @@ -76,6 +91,87 @@ export default function Annotation({
annotation.anchorOffset = new DOMPoint(anchorOffsetX, anchorOffsetY);
}, [annotation, anchorOffsetX, anchorOffsetY]);

// CalloutOffset
useEffect(() => {
if (!annotation) return;
annotation.calloutOffset = new DOMPoint(calloutOffsetX, calloutOffsetY);
}, [annotation, calloutOffsetX, calloutOffsetY]);

const calloutLeftAccessoryRef = useRef<HTMLDivElement>(null);
const calloutRightAccessoryRef = useRef<HTMLDivElement>(null);
const calloutContentRef = useRef<HTMLDivElement>(null);
const calloutElementRef = useRef<HTMLDivElement>(null);

// Callout
useEffect(() => {
if (!annotation) return;

const callOutObj: mapkit.AnnotationCalloutDelegate = {};
if (calloutElement && calloutElementRef.current !== null) {
// @ts-expect-error
callOutObj.calloutElementForAnnotation = () => calloutElementRef.current;
}
if (
calloutLeftAccessory
&& calloutLeftAccessoryRef.current !== null
) {
// @ts-expect-error
callOutObj.calloutLeftAccessoryForAnnotation = () => calloutLeftAccessoryRef
.current;
}
if (
calloutRightAccessory
&& calloutRightAccessoryRef.current !== null
) {
// @ts-expect-error
callOutObj.calloutRightAccessoryForAnnotation = () => calloutRightAccessoryRef
.current;
}
if (calloutContent && calloutContentRef.current !== null) {
// @ts-expect-error
callOutObj.calloutContentForAnnotation = () => calloutContentRef.current;
}
if (Object.keys(callOutObj).length > 0) {
annotation.callout = callOutObj;
} else {
// @ts-expect-error
delete annotation.callout;
}
}, [
annotation,
calloutElement,
calloutLeftAccessory,
calloutRightAccessory,
calloutContent,
calloutElementRef.current,
calloutLeftAccessoryRef.current,
calloutRightAccessoryRef.current,
calloutContentRef.current,
]);

// Collision Mode
useEffect(() => {
if (!annotation) return;

if (collisionMode === 'Circle') {
annotation.collisionMode = mapkit.Annotation.CollisionMode.Circle;
} else if (collisionMode === 'Rectangle') {
annotation.collisionMode = mapkit.Annotation.CollisionMode.Rectangle;
} else {
// @ts-ignore
delete annotation.collisionMode;
}
}, [annotation, collisionMode]);

// Display Priority
useEffect(() => {
if (!annotation) return;
// @ts-ignore
if (displayPriority === undefined) { delete annotation.displayPriority; return; }
// @ts-ignore
annotation.displayPriority = toMapKitDisplayPriority(displayPriority);
}, [annotation, displayPriority]);

// Simple values properties
const properties = {
title,
Expand All @@ -90,7 +186,10 @@ export default function Annotation({
draggable,
enabled,
visible,

clusteringIdentifier,

calloutEnabled,
};
Object.entries(properties).forEach(([propertyName, prop]) => {
useEffect(() => {
Expand Down Expand Up @@ -124,5 +223,48 @@ export default function Annotation({
forwardMapkitEvent(annotation, 'drag-end', onDragEnd, dragEndParameters);
forwardMapkitEvent(annotation, 'dragging', onDragging, draggingParameters);

if (calloutEnabled) {
return (
<>
{calloutContent !== undefined && createPortal(
<CalloutContainer
ref={calloutContentRef}
type="content"
>
{calloutContent}
</CalloutContainer>,
document.body,
)}
{calloutLeftAccessory !== undefined && createPortal(
<CalloutContainer
ref={calloutLeftAccessoryRef}
type="left"
>
{calloutLeftAccessory}
</CalloutContainer>,
document.body,
)}
{calloutRightAccessory !== undefined && createPortal(
<CalloutContainer
ref={calloutRightAccessoryRef}
type="right"
>
{calloutRightAccessory}
</CalloutContainer>,
document.body,
)}
{calloutElement !== undefined && createPortal(
<CalloutContainer
ref={calloutElementRef}
type="container"
>
{calloutElement}
</CalloutContainer>,
document.body,
)}
{createPortal(children, contentEl)}
</>
);
}
return createPortal(children, contentEl);
}
68 changes: 67 additions & 1 deletion src/components/AnnotationProps.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ReactNode } from 'react';
import { Coordinate } from '../util/parameters';

export default interface AnnotationProps {
Expand Down Expand Up @@ -142,5 +143,70 @@ export default interface AnnotationProps {
* An annotation needs a clusteringIdentifier to be part of an annotation cluster.
* @see {@link https://developer.apple.com/documentation/mapkitjs/mapkit/annotations/clustering_annotations}
*/
clusteringIdentifier?: string | null
clusteringIdentifier?: string | null;

/**
* A mode that determines the shape of the collision frame.
* Rectangle | Circle | None
* @see {@link https://developer.apple.com/documentation/mapkitjs/mapkit/annotation/2973822-collisionmode}
* @see {@link https://developer.apple.com/documentation/mapkitjs/mapkit/annotation/collisionmode}
*/
collisionMode?: 'Rectangle' | 'Circle' | null;

/**
* A numeric hint that the map uses to prioritize how it displays annotations.
*
* Is either any number from `0` to `1000`,
* or a preset value: `"low"` (250), `"high"` (750), or `"required"` (1000).
*
* @see {@link https://developer.apple.com/documentation/mapkitjs/mapkit/annotation/2973825-displaypriority}
*/
displayPriority?: number | 'low' | 'high' | 'required';

/**
* An X offset that changes the annotation callout’s default placement.
* @see {@link https://developer.apple.com/documentation/mapkitjs/mapkit/annotation/2973821-calloutoffset}
*/
calloutOffsetX?: number;

/**
* An Y offset that changes the annotation callout’s default placement.
* @see {@link https://developer.apple.com/documentation/mapkitjs/mapkit/annotation/2973821-calloutoffset}
*/
calloutOffsetY?: number;

/**
* A Boolean value that determines whether the map shows an annotation’s callout.
* If the title is empty, the framework can’t show the standard callout even if property is true.
* @see {@link https://developer.apple.com/documentation/mapkitjs/mapkit/annotation/2973820-calloutenabled}
*/
calloutEnabled?: boolean;

/**
* Returns an element to use as a custom accessory on the left side of the callout content area.
*
* @see {@link https://developer.apple.com/documentation/mapkitjs/annotationcalloutdelegate/2991150-calloutleftaccessoryforannotatio}
*/
calloutLeftAccessory?: ReactNode;

/**
* Returns an element to use as a custom accessory on the right side of the callout content area.
*
* @see {@link https://developer.apple.com/documentation/mapkitjs/annotationcalloutdelegate/2991151-calloutrightaccessoryforannotati}
*/
calloutRightAccessory?: ReactNode;

/**
* Returns custom content for the callout bubble.
*
* @see {@link https://developer.apple.com/documentation/mapkitjs/annotationcalloutdelegate/2991148-calloutcontentforannotation}
*/
calloutContent?: ReactNode;

/**
* Returns an element representing a custom callout.
*
* @see {@link https://developer.apple.com/documentation/mapkitjs/annotationcalloutdelegate/2991148-calloutcontentforannotation}
*/
calloutElement?: ReactNode;
}
15 changes: 15 additions & 0 deletions src/components/CalloutContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React, { ReactNode } from 'react';

const CalloutContainer = React.forwardRef<
HTMLDivElement,
React.PropsWithChildren<{ children: ReactNode, type?: string }>
>((
{ children, type = 'container' },
ref,
) => (
<div className={`mk-callout-${type}`} ref={ref}>
{children}
</div>
));

export default CalloutContainer;
Loading

0 comments on commit 2815a52

Please sign in to comment.