maplibre / maplibre/maplibre-react-native
Android: Marker content is invisible to accessibility services (content wrapper keeps 0x0 bounds)
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 661
- Forks
- 124
- Avg merge
- 1d 2h
- Merged PRs (30d)
- 30
Description
**Environment**
- `@maplibre/maplibre-react-native` 11.3.4 (also present on current `main`)
- React Native 0.85.3, New Architecture, Expo SDK 56 dev client
- Android 16 emulator (Pixel), also reproduced on a physical device
**What happens**
Any React view passed as `Marker` children renders on the map but is completely missing from the Android accessibility tree. TalkBack cannot focus the marker, `accessible` / `accessibilityLabel` / `accessibilityRole` on the marker content are never surfaced, and a `uiautomator dump` shows no node for the marker at all. For map-centric apps this makes every marker invisible to screen-reader users.
**Why**
`MLRNMarkerView.tryAddToMarkerViewManager` re-parents the React child into an `MLRNMarkerViewContent` wrapper (`WRAP_CONTENT`) and `MarkerViewManager.addMarker` adds that wrapper directly to the `MapView`. From then on the wrapper is positioned purely via `view.x` / `view.y` in `updateMarkerPosition`; nothing ever calls `measure()` / `layout()` on it. The wrapper keeps 0x0 bounds forever. The content still draws because clipping is disabled everywhere, but Android drops zero-sized views (and their entire subtrees) from the accessibility node tree, so the marker content never reaches accessibility services.
You can verify with layout-bounds debugging or `adb shell uiautomator dump`: the marker child views have real pixels on screen but no bounds and no accessibility nodes.
**Fix we run in production (via pnpm patch)**
Give the wrapper its content's real bounds whenever they drift, right where the anchor math already reads the content size:
```kotlin
// MarkerViewManager.updateMarkerPosition
val (viewWidth, viewHeight) = view.getContentSize()
val w = viewWidth.toInt()
val h = viewHeight.toInt()
if (w > 0 && h > 0 && (view.width != w || view.height != h)) {
view.measure(
View.MeasureSpec.makeMeasureSpec(w, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(h, View.MeasureSpec.EXACTLY),
)
view.layout(0, 0, w, h)
}
```
With this, every marker appears in the accessibility tree with correct screen bounds and its `accessibilityLabel` is spoken by TalkBack.
**One interaction gotcha the fix exposes**
Once the wrapper has real bounds, a clickable React child inside the marker (a `Pressable`, or any `accessible` touchable) starts consuming raw touch events before MapLibre's gesture detector runs, so `onMapClick -> findMarkerAtPoint -> markerView.onPress` never fires and marker presses die. Since marker presses are hit-tested by the map click handler rather than by normal view touch dispatch, we paired the layout fix with:
```kotlin
// MLRNMarkerViewContent
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean = false
```
Both changes together have been running in production for us with no regressions: markers are focusable and spoken by TalkBack, and press handling is unchanged.
Happy to turn this into a PR if the approach looks right to maintainers.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in MarkerViewManager.updateMarkerPosition and inspect MLRNMarkerView.tryAddToMarkerViewManager and MLRNMarkerViewContent to understand wrapper sizing and touch dispatch. Verify the marker wrapper receives real bounds, appears in the Android accessibility tree with its React accessibility properties, and preserves marker press handling through the map click path.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- android, kotlin, react-native
- Domain
- accessibility, mobile
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100