From 702e214ebe752a8f398f0380cbd4a82e680ba1f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Sun, 17 May 2026 22:05:01 +0200 Subject: [PATCH 1/6] feat: add PointAnnotationManager component with slot support New component that configures the shared point annotation manager. Supports slot prop for positioning annotations in the Standard style layer stack. --- .../java/com/rnmapbox/rnmbx/RNMBXPackage.kt | 2 + .../RNMBXPointAnnotationManagerView.kt | 26 +++++++ .../RNMBXPointAnnotationManagerViewManager.kt | 33 +++++++++ docs/docs.json | 24 +++++++ ...RNMBXPointAnnotationManagerComponentView.h | 15 ++++ ...NMBXPointAnnotationManagerComponentView.mm | 70 +++++++++++++++++++ ...XPointAnnotationManagerComponentView.swift | 25 +++++++ package.json | 4 ++ src/Mapbox.native.ts | 1 + src/components/PointAnnotationManager.tsx | 29 ++++++++ ...BXPointAnnotationManagerNativeComponent.ts | 18 +++++ 11 files changed, 247 insertions(+) create mode 100644 android/src/main/java/com/rnmapbox/rnmbx/components/annotation/RNMBXPointAnnotationManagerView.kt create mode 100644 android/src/main/java/com/rnmapbox/rnmbx/components/annotation/RNMBXPointAnnotationManagerViewManager.kt create mode 100644 ios/RNMBX/RNMBXPointAnnotationManagerComponentView.h create mode 100644 ios/RNMBX/RNMBXPointAnnotationManagerComponentView.mm create mode 100644 ios/RNMBX/RNMBXPointAnnotationManagerComponentView.swift create mode 100644 src/components/PointAnnotationManager.tsx create mode 100644 src/specs/RNMBXPointAnnotationManagerNativeComponent.ts diff --git a/android/src/main/java/com/rnmapbox/rnmbx/RNMBXPackage.kt b/android/src/main/java/com/rnmapbox/rnmbx/RNMBXPackage.kt index f82fadd32d..0b9bf05813 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/RNMBXPackage.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/RNMBXPackage.kt @@ -11,6 +11,7 @@ import com.rnmapbox.rnmbx.components.annotation.RNMBXCalloutManager import com.rnmapbox.rnmbx.components.annotation.RNMBXMarkerViewContentManager import com.rnmapbox.rnmbx.components.annotation.RNMBXMarkerViewManager import com.rnmapbox.rnmbx.components.annotation.RNMBXPointAnnotationManager +import com.rnmapbox.rnmbx.components.annotation.RNMBXPointAnnotationManagerViewManager import com.rnmapbox.rnmbx.components.annotation.RNMBXPointAnnotationModule import com.rnmapbox.rnmbx.components.camera.RNMBXCameraManager import com.rnmapbox.rnmbx.components.camera.RNMBXCameraModule @@ -135,6 +136,7 @@ class RNMBXPackage : TurboReactPackage() { managers.add(RNMBXMarkerViewManager(reactApplicationContext)) managers.add(RNMBXMarkerViewContentManager(reactApplicationContext)) managers.add(RNMBXPointAnnotationManager(reactApplicationContext, getViewTagResolver(reactApplicationContext, "RNMBXPointAnnotationManager"))) + managers.add(RNMBXPointAnnotationManagerViewManager(reactApplicationContext)) managers.add(RNMBXCalloutManager()) managers.add(RNMBXNativeUserLocationManager()) managers.add(RNMBXCustomLocationProviderManager()) diff --git a/android/src/main/java/com/rnmapbox/rnmbx/components/annotation/RNMBXPointAnnotationManagerView.kt b/android/src/main/java/com/rnmapbox/rnmbx/components/annotation/RNMBXPointAnnotationManagerView.kt new file mode 100644 index 0000000000..f97d17ef77 --- /dev/null +++ b/android/src/main/java/com/rnmapbox/rnmbx/components/annotation/RNMBXPointAnnotationManagerView.kt @@ -0,0 +1,26 @@ +package com.rnmapbox.rnmbx.components.annotation + +import android.content.Context +import com.rnmapbox.rnmbx.components.AbstractMapFeature +import com.rnmapbox.rnmbx.components.RemovalReason +import com.rnmapbox.rnmbx.components.mapview.RNMBXMapView + +class RNMBXPointAnnotationManagerView(context: Context) : AbstractMapFeature(context) { + var slot: String? = null + set(value) { + field = value + applySlot() + } + + private fun applySlot() { + withMapView { mapView -> + slot?.let { mapView.pointAnnotations?.manager?.slot = it } + ?: run { mapView.pointAnnotations?.manager?.slot = null } + } + } + + override fun addToMap(mapView: RNMBXMapView) { + super.addToMap(mapView) + applySlot() + } +} diff --git a/android/src/main/java/com/rnmapbox/rnmbx/components/annotation/RNMBXPointAnnotationManagerViewManager.kt b/android/src/main/java/com/rnmapbox/rnmbx/components/annotation/RNMBXPointAnnotationManagerViewManager.kt new file mode 100644 index 0000000000..b4edbb202b --- /dev/null +++ b/android/src/main/java/com/rnmapbox/rnmbx/components/annotation/RNMBXPointAnnotationManagerViewManager.kt @@ -0,0 +1,33 @@ +package com.rnmapbox.rnmbx.components.annotation + +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.common.MapBuilder +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.annotations.ReactProp +import com.facebook.react.viewmanagers.RNMBXPointAnnotationManagerManagerInterface +import com.rnmapbox.rnmbx.components.AbstractEventEmitter + +class RNMBXPointAnnotationManagerViewManager(context: ReactApplicationContext) : + AbstractEventEmitter(context), + RNMBXPointAnnotationManagerManagerInterface { + override fun customEvents(): Map? { + return MapBuilder.builder().build() + } + + override fun getName(): String { + return REACT_CLASS + } + + override fun createViewInstance(context: ThemedReactContext): RNMBXPointAnnotationManagerView { + return RNMBXPointAnnotationManagerView(context) + } + + companion object { + const val REACT_CLASS = "RNMBXPointAnnotationManager" + } + + @ReactProp(name = "slot") + override fun setSlot(view: RNMBXPointAnnotationManagerView, value: String?) { + view.slot = value + } +} diff --git a/docs/docs.json b/docs/docs.json index 399a40f854..0074a55653 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -6529,6 +6529,30 @@ "relPath": "src/components/PointAnnotation.tsx", "name": "PointAnnotation" }, + "PointAnnotationManager": { + "description": "Configures the shared PointAnnotation manager for the parent MapView.\nWrap PointAnnotation components as children.", + "displayName": "PointAnnotationManager", + "methods": [], + "props": [ + { + "name": "slot", + "required": false, + "type": "'bottom' \\| 'middle' \\| 'top' \\| (string & {})", + "default": "none", + "description": "The slot in the style layer stack to position the annotation layer.\nUse with Mapbox Standard style to control layer ordering." + }, + { + "name": "children", + "required": false, + "type": "ReactNode", + "default": "none", + "description": "FIX ME NO DESCRIPTION" + } + ], + "fileNameWithExt": "PointAnnotationManager.tsx", + "relPath": "src/components/PointAnnotationManager.tsx", + "name": "PointAnnotationManager" + }, "Rain": { "description": "", "displayName": "Rain", diff --git a/ios/RNMBX/RNMBXPointAnnotationManagerComponentView.h b/ios/RNMBX/RNMBXPointAnnotationManagerComponentView.h new file mode 100644 index 0000000000..70e7b20e63 --- /dev/null +++ b/ios/RNMBX/RNMBXPointAnnotationManagerComponentView.h @@ -0,0 +1,15 @@ +#ifdef __cplusplus + +#import + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface RNMBXPointAnnotationManagerComponentView : RCTViewComponentView + +@end + +NS_ASSUME_NONNULL_END +#endif // __cplusplus diff --git a/ios/RNMBX/RNMBXPointAnnotationManagerComponentView.mm b/ios/RNMBX/RNMBXPointAnnotationManagerComponentView.mm new file mode 100644 index 0000000000..a6713efc1e --- /dev/null +++ b/ios/RNMBX/RNMBXPointAnnotationManagerComponentView.mm @@ -0,0 +1,70 @@ + +#import "RNMBXPointAnnotationManagerComponentView.h" +#import "RNMBXFabricHelpers.h" + +#import +#import + +#import +#import +#import +#import + +#import "rnmapbox_maps-Swift.pre.h" + +using namespace facebook::react; + +@interface RNMBXPointAnnotationManagerComponentView () +@end + +@implementation RNMBXPointAnnotationManagerComponentView { + RNMBXPointAnnotationManagerView *_view; +} + +// Needed because of this: https://github.com/facebook/react-native/pull/37274 ++ (void)load { + [super load]; +} + +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + static const auto defaultProps = + std::make_shared(); + _props = defaultProps; + [self prepareView]; + } + + return self; +} + +- (void)prepareView { + _view = [[RNMBXPointAnnotationManagerView alloc] init]; + self.contentView = _view; +} + +#pragma mark - RCTComponentViewProtocol + ++ (ComponentDescriptorProvider)componentDescriptorProvider { + return concreteComponentDescriptorProvider< + RNMBXPointAnnotationManagerComponentDescriptor>(); +} + +- (void)updateProps:(const Props::Shared &)props + oldProps:(const Props::Shared &)oldProps { + const auto &newProps = static_cast(*props); + + RNMBX_OPTIONAL_PROP_NSString(slot) + + [super updateProps:props oldProps:oldProps]; +} + +- (void)prepareForRecycle { + [super prepareForRecycle]; + [self prepareView]; +} + +@end + +Class RNMBXPointAnnotationManagerCls(void) { + return RNMBXPointAnnotationManagerComponentView.class; +} diff --git a/ios/RNMBX/RNMBXPointAnnotationManagerComponentView.swift b/ios/RNMBX/RNMBXPointAnnotationManagerComponentView.swift new file mode 100644 index 0000000000..325c5cde8b --- /dev/null +++ b/ios/RNMBX/RNMBXPointAnnotationManagerComponentView.swift @@ -0,0 +1,25 @@ +import MapboxMaps + +@objc(RNMBXPointAnnotationManagerView) +open class RNMBXPointAnnotationManagerView: RNMBXMapComponentBase { + @objc public var slot: String? = nil { + didSet { + applySlot() + } + } + + private func applySlot() { + withRNMBXMapView { map in + if let slot = self.slot { + map.pointAnnotationManager.manager.slot = Slot(rawValue: slot) + } else { + map.pointAnnotationManager.manager.slot = nil + } + } + } + + public override func addToMap(_ map: RNMBXMapView, style: Style) { + super.addToMap(map, style: style) + applySlot() + } +} diff --git a/package.json b/package.json index 2f685de63e..c41a4f32fc 100644 --- a/package.json +++ b/package.json @@ -259,6 +259,9 @@ "RNMBXNativeUserLocation": { "className": "RNMBXNativeUserLocationComponentView" }, + "RNMBXPointAnnotationManager": { + "className": "RNMBXPointAnnotationManagerComponentView" + }, "RNMBXPointAnnotation": { "className": "RNMBXPointAnnotationComponentView" }, @@ -328,6 +331,7 @@ "RNMBXModelLayer": "RNMBXModelLayerComponentView", "RNMBXModels": "RNMBXModelsComponentView", "RNMBXNativeUserLocation": "RNMBXNativeUserLocationComponentView", + "RNMBXPointAnnotationManager": "RNMBXPointAnnotationManagerComponentView", "RNMBXPointAnnotation": "RNMBXPointAnnotationComponentView", "RNMBXRain": "RNMBXRainComponentView", "RNMBXRasterArraySource": "RNMBXRasterArraySourceComponentView", diff --git a/src/Mapbox.native.ts b/src/Mapbox.native.ts index 81abb78aff..69d693e20c 100644 --- a/src/Mapbox.native.ts +++ b/src/Mapbox.native.ts @@ -18,6 +18,7 @@ export { } from './components/MapView'; export { default as Light } from './components/Light'; export { default as PointAnnotation } from './components/PointAnnotation'; +export { default as PointAnnotationManager } from './components/PointAnnotationManager'; export { default as Annotation } from './components/Annotation'; export { default as Callout } from './components/Callout'; export { default as StyleImport } from './components/StyleImport'; diff --git a/src/components/PointAnnotationManager.tsx b/src/components/PointAnnotationManager.tsx new file mode 100644 index 0000000000..90ce2f5bf7 --- /dev/null +++ b/src/components/PointAnnotationManager.tsx @@ -0,0 +1,29 @@ +import { type ReactNode } from 'react'; + +import NativePointAnnotationManager from '../specs/RNMBXPointAnnotationManagerNativeComponent'; + +type Slot = 'bottom' | 'middle' | 'top'; + +type Props = { + /** + * The slot in the style layer stack to position the annotation layer. + * Use with Mapbox Standard style to control layer ordering. + */ + slot?: Slot | (string & {}); + + children?: ReactNode; +}; + +/** + * Configures the shared PointAnnotation manager for the parent MapView. + * Wrap PointAnnotation components as children. + */ +const PointAnnotationManager = (props: Props) => { + return ( + + {props.children} + + ); +}; + +export default PointAnnotationManager; diff --git a/src/specs/RNMBXPointAnnotationManagerNativeComponent.ts b/src/specs/RNMBXPointAnnotationManagerNativeComponent.ts new file mode 100644 index 0000000000..11a50911bc --- /dev/null +++ b/src/specs/RNMBXPointAnnotationManagerNativeComponent.ts @@ -0,0 +1,18 @@ +import type { HostComponent, ViewProps } from 'react-native'; +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; + +import type { UnsafeMixed } from './codegenUtils'; + +// see https://github.com/rnmapbox/maps/wiki/FabricOptionalProp +type OptionalProp = UnsafeMixed; + +type Slot = 'bottom' | 'middle' | 'top'; + +export interface NativeProps extends ViewProps { + slot?: OptionalProp; +} + +// @ts-ignore-error - Codegen requires single cast but TypeScript prefers double cast +export default codegenNativeComponent( + 'RNMBXPointAnnotationManager', +) as HostComponent; From f648f34528bfe495dcd6f6520c05eed2338cce07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Mon, 18 May 2026 06:15:02 +0200 Subject: [PATCH 2/6] Add example and fix interface test --- __tests__/interface.test.js | 1 + .../PointAnnotationManagerSlot.tsx | 91 +++++++++++++++++++ example/src/examples/Annotations/index.js | 1 + 3 files changed, 93 insertions(+) create mode 100644 example/src/examples/Annotations/PointAnnotationManagerSlot.tsx diff --git a/__tests__/interface.test.js b/__tests__/interface.test.js index d7e7299680..45b6ed7cee 100644 --- a/__tests__/interface.test.js +++ b/__tests__/interface.test.js @@ -9,6 +9,7 @@ describe('Public Interface', () => { 'StyleSheet', 'Light', 'PointAnnotation', + 'PointAnnotationManager', 'MarkerView', 'Annotation', 'Callout', diff --git a/example/src/examples/Annotations/PointAnnotationManagerSlot.tsx b/example/src/examples/Annotations/PointAnnotationManagerSlot.tsx new file mode 100644 index 0000000000..bfa5a4b2e6 --- /dev/null +++ b/example/src/examples/Annotations/PointAnnotationManagerSlot.tsx @@ -0,0 +1,91 @@ +import { useState } from 'react'; +import { View, Text, StyleSheet } from 'react-native'; +import { + Camera, + MapView, + PointAnnotation, + PointAnnotationManager, +} from '@rnmapbox/maps'; +import { Button } from '@rneui/base'; + +import { ExampleWithMetadata } from '../common/ExampleMetadata'; // exclude-from-doc + +const styles = StyleSheet.create({ + map: { flex: 1 }, + pin: { + width: 30, + height: 30, + borderRadius: 15, + justifyContent: 'center', + alignItems: 'center', + }, + label: { color: 'white', fontWeight: 'bold', fontSize: 12 }, + buttons: { + flexDirection: 'row', + justifyContent: 'center', + padding: 8, + gap: 8, + }, +}); + +const COORDS: [number, number][] = [ + [-74.00597, 40.71427], + [-74.0065, 40.7128], + [-74.0045, 40.7155], +]; + +const PointAnnotationManagerSlot = () => { + const [slot, setSlot] = useState('middle'); + + return ( + <> + + + + {COORDS.map((coord, i) => ( + + + {i + 1} + + + ))} + + + +