Skip to content

fix(point annotation): selected property should select/deselect#4165

Merged
mfazekas merged 4 commits intomainfrom
fix/point-annotation-selected
Mar 1, 2026
Merged

fix(point annotation): selected property should select/deselect#4165
mfazekas merged 4 commits intomainfrom
fix/point-annotation-selected

Conversation

@mfazekas
Copy link
Contributor

@mfazekas mfazekas commented Mar 1, 2026

Rebased version of #4036 on current main. Adds selected prop to PointAnnotation to programmatically select/deselect annotations. Drops old-arch changes (no longer needed) and adapts iOS ComponentView to use macro-based prop conversion.

Also fixes callout not rendering on iOS (Fabric) — children were mounted into the ComponentView instead of the contentView, producing transparent snapshots.

image

Fixes #4034
Closes #4036

Minimal reproducer
import { useState } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { Camera, MapView, PointAnnotation, Callout } from '@rnmapbox/maps';
import { Button } from '@rneui/base';

export default function SelectedPropExample() {
  const [selected, setSelected] = useState(false);

  return (
    <>
      <MapView style={styles.map} deselectAnnotationOnTap>
        <Camera defaultSettings={{ centerCoordinate: [-73.99155, 40.73581], zoomLevel: 16 }} />
        <PointAnnotation
          id="test"
          coordinate={[-73.99155, 40.73581]}
          title="Test"
          selected={selected}
          onSelected={() => {
            console.log('onSelected');
            setSelected(true);
          }}
          onDeselected={() => {
            console.log('onDeselected');
            setSelected(false);
          }}
        >
          <View style={styles.pin} />
          <Callout title="Hello from callout" />
        </PointAnnotation>
      </MapView>
      <Button onPress={() => setSelected(s => !s)}>
        {selected ? 'Deselect' : 'Select'}
      </Button>
    </>
  );
}

const styles = StyleSheet.create({
  map: { flex: 1 },
  pin: {
    width: 30, height: 30,
    backgroundColor: 'white',
    borderRadius: 15,
    borderWidth: StyleSheet.hairlineWidth,
    borderColor: 'rgba(0,0,0,0.45)',
  },
});
Full example (ShowPointAnnotation.tsx)
import { useRef, useState } from 'react';
import { View, Text, StyleSheet, Image } from 'react-native';
import {
  Callout,
  Camera,
  FillLayer,
  MapView,
  PointAnnotation,
  ShapeSource,
  getAnnotationsLayerID,
} from '@rnmapbox/maps';
import { Point, Position } from 'geojson';
import { Button } from '@rneui/base';

import Bubble from '../common/Bubble';

const ANNOTATION_SIZE = 40;

const styles = {
  annotationContainer: {
    alignItems: 'center',
    backgroundColor: 'white',
    borderColor: 'rgba(0, 0, 0, 0.45)',
    borderRadius: ANNOTATION_SIZE / 2,
    borderWidth: StyleSheet.hairlineWidth,
    height: ANNOTATION_SIZE,
    justifyContent: 'center',
    overflow: 'hidden',
    width: ANNOTATION_SIZE,
  },
  matchParent: {
    flex: 1,
  },
} as const;

type AnnotationWithRemoteImageProps = {
  id: string;
  title: string;
  coordinate: Position;
};

const AnnotationWithRemoteImage = ({
  id,
  coordinate,
  title,
}: AnnotationWithRemoteImageProps) => {
  const pointAnnotation = useRef<PointAnnotation>(null);

  return (
    <PointAnnotation
      id={id}
      coordinate={coordinate}
      title={title}
      draggable
      onSelected={(feature) =>
        console.log('onSelected:', feature.id, feature.geometry.coordinates)
      }
      onDrag={(feature) =>
        console.log('onDrag:', feature.id, feature.geometry.coordinates)
      }
      onDragStart={(feature) =>
        console.log('onDragStart:', feature.id, feature.geometry.coordinates)
      }
      onDragEnd={(feature) =>
        console.log('onDragEnd:', feature.id, feature.geometry.coordinates)
      }
      ref={pointAnnotation}
    >
      <View style={styles.annotationContainer}>
        <Image
          source={{ uri: 'https://reactnative.dev/img/tiny_logo.png' }}
          style={{ width: ANNOTATION_SIZE, height: ANNOTATION_SIZE }}
          onLoad={() => pointAnnotation.current?.refresh()}
          fadeDuration={0}
        />
      </View>
      <Callout title="This is a sample loading a remote image" />
    </PointAnnotation>
  );
};

const ShowPointAnnotation = () => {
  const [coordinates, setCoordinates] = useState([
    [-73.99155, 40.73581],
    [-73.99155, 40.73681],
  ]);
  const [selected, setSelected] = useState(false);
  const [layerRendering, setLayerRendering] = useState<'below' | 'above'>(
    'below',
  );

  const renderAnnotations = () => {
    const items = [];

    for (let i = 0; i < coordinates.length; i++) {
      const coordinate = coordinates[i]!;
      const title = `Lon: ${coordinate[0]} Lat: ${coordinate[1]}`;
      const id = `pointAnnotation${i}`;

      if (i % 2 === 1) {
        items.push(
          null,
          <AnnotationWithRemoteImage
            key={id}
            id={id}
            coordinate={coordinate}
            title={title}
          />,
        );
      } else {
        items.push(
          null,
          <PointAnnotation
            key={id}
            id={id}
            coordinate={coordinate}
            title={title}
            selected={i === 0 ? selected : undefined}
            onSelected={() => {
              console.log('onSelected:', id);
              if (i === 0) setSelected(true);
            }}
            onDeselected={() => {
              console.log('onDeselected:', id);
              if (i === 0) setSelected(false);
            }}
          >
            <View style={styles.annotationContainer} />
            <Callout title="This is an empty example" />
          </PointAnnotation>,
        );
      }
    }

    return items;
  };

  return (
    <>
      <MapView
        onPress={(feature) => {
          setCoordinates((prevState) => [
            ...prevState,
            (feature.geometry as Point).coordinates,
          ]);
        }}
        style={styles.matchParent}
        deselectAnnotationOnTap={true}
      >
        <Camera
          defaultSettings={{ centerCoordinate: coordinates[0], zoomLevel: 16 }}
        />

        {renderAnnotations()}

        <ShapeSource
          id="polygon"
          shape={{
            coordinates: [
              [
                [-73.98813787946587, 40.73199795542578],
                [-73.98313197853199, 40.7388685230859],
                [-73.98962548210226, 40.74155214586244],
                [-73.9945841575561, 40.73468185536569],
                [-73.98813787946587, 40.73199795542578],
              ],
            ],
            type: 'Polygon',
          }}
        >
          <FillLayer
            id="polygon"
            {...{
              [layerRendering + 'LayerID']:
                getAnnotationsLayerID('PointAnnotations'),
            }}
            style={{
              fillColor: 'rgba(255, 0, 0, 0.5)',
              fillOutlineColor: 'red',
            }}
          />
        </ShapeSource>
      </MapView>

      <Bubble>
        <Text style={{ marginBottom: 10 }}>
          Click to add a point annotation
        </Text>
        <Button onPress={() => setSelected((s) => !s)}>
          {selected ? 'Deselect' : 'Select'} First Annotation
        </Button>
        <Button
          onPress={() =>
            setLayerRendering(
              (prevState) =>
                (({ above: 'below', below: 'above' }) as const)[prevState],
            )
          }
        >
          Render Polygon {{ above: 'below', below: 'above' }[layerRendering]}
        </Button>
      </Bubble>
    </>
  );
};

export default ShowPointAnnotation;

…nt annotation

Co-Authored-By: Miklós Fazekas <mfazekas@szemafor.com>
Callout children were mounted into the ComponentView instead of the
contentView (RNMBXCallout), producing a transparent snapshot. Override
mountChildComponentView in RNMBXCalloutComponentView to insert children
into the contentView. Also set iconAllowOverlap on the callout annotation
manager so callouts are not hidden by symbol collision.
@mfazekas mfazekas merged commit 6ce02de into main Mar 1, 2026
7 checks passed
@mfazekas mfazekas deleted the fix/point-annotation-selected branch March 1, 2026 14:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: PointAnnotation selected not allowing controlled values

1 participant