From 75352542256287ae5039f81558deb27a5aa4cad3 Mon Sep 17 00:00:00 2001 From: Himanshu Gupta Date: Wed, 17 Jun 2026 00:47:51 +0300 Subject: [PATCH 1/3] fix: keep maps as single tab focus targets --- .../compose/GoogleMapFocusTraversalTests.kt | 76 +++++++++++++++++++ .../google/maps/android/compose/GoogleMap.kt | 4 + 2 files changed, 80 insertions(+) create mode 100644 maps-app/src/androidTest/java/com/google/maps/android/compose/GoogleMapFocusTraversalTests.kt diff --git a/maps-app/src/androidTest/java/com/google/maps/android/compose/GoogleMapFocusTraversalTests.kt b/maps-app/src/androidTest/java/com/google/maps/android/compose/GoogleMapFocusTraversalTests.kt new file mode 100644 index 00000000..fd3a5871 --- /dev/null +++ b/maps-app/src/androidTest/java/com/google/maps/android/compose/GoogleMapFocusTraversalTests.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.maps.android.compose + +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertIsFocused +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onAllNodesWithTag +import androidx.compose.ui.test.performKeyInput +import androidx.compose.ui.test.pressKey +import androidx.compose.ui.test.requestFocus +import com.google.android.gms.maps.model.LatLng +import org.junit.Rule +import org.junit.Test + +class GoogleMapFocusTraversalTests { + @get:Rule + val composeTestRule = createComposeRule() + + private val mapItems = listOf( + MapListItem(id = "1", location = LatLng(1.23, 4.56), zoom = 10f, title = "Item 1"), + MapListItem(id = "2", location = LatLng(7.89, 0.12), zoom = 12f, title = "Item 2"), + MapListItem(id = "3", location = LatLng(3.45, 6.78), zoom = 11f, title = "Item 3"), + ) + + private fun initMaps() { + check(hasValidApiKey) { "Maps API key not specified" } + + composeTestRule.setContent { + MapsInLazyColumn( + mapItems, + lazyListState = rememberLazyListState(), + onMapLoaded = {}, + ) + } + + composeTestRule.waitUntil(timeoutMillis = 5_000) { + composeTestRule + .onAllNodesWithTag("Map", useUnmergedTree = true) + .fetchSemanticsNodes() + .size >= 2 + } + } + + @OptIn(ExperimentalTestApi::class) + @Test + fun tabFromMapFocusesNextMap() { + initMaps() + + val visibleMaps = composeTestRule.onAllNodesWithTag("Map", useUnmergedTree = true) + visibleMaps[0].requestFocus() + visibleMaps[0].assertIsFocused() + + visibleMaps[0].performKeyInput { + pressKey(Key.Tab) + } + + visibleMaps[1].assertIsFocused() + } +} diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 7a96b86e..0e3d2ec2 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -21,6 +21,7 @@ import android.content.res.Configuration import android.location.Location import android.os.Bundle import android.view.View +import android.view.ViewGroup import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Composable @@ -155,6 +156,9 @@ public fun GoogleMap( val options = googleMapOptionsFactory() cameraPositionState.isLiteMode = options.liteMode == true mapViewFactory(context, options).also { mapView -> + mapView.isFocusable = true + mapView.descendantFocusability = ViewGroup.FOCUS_BEFORE_DESCENDANTS + val componentCallbacks = object : ComponentCallbacks2 { override fun onConfigurationChanged(newConfig: Configuration) {} From 40a08b383daca8320d4f547931d7e6fc493a808a Mon Sep 17 00:00:00 2001 From: Dale Hawkins <107309+dkhawk@users.noreply.github.com> Date: Fri, 26 Jun 2026 00:06:11 +0000 Subject: [PATCH 2/3] fix: make GoogleMap composable focusable in Compose This ensures that the Compose focus system can successfully target the AndroidView wrapper, allowing focus traversal (TAB) to work correctly and resolving test failures. TAG=agy CONV=c8ea8596-9a4a-488b-885a-df4f8d27c209 --- .../src/main/java/com/google/maps/android/compose/GoogleMap.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 0e3d2ec2..1a567fb1 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -22,6 +22,7 @@ import android.location.Location import android.os.Bundle import android.view.View import android.view.ViewGroup +import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Composable @@ -151,7 +152,7 @@ public fun GoogleMap( val parentCompositionScope = rememberCoroutineScope() AndroidView( - modifier = modifier, + modifier = modifier.focusable(), factory = { context -> val options = googleMapOptionsFactory() cameraPositionState.isLiteMode = options.liteMode == true From 752a1a824a682806df988bd2a4344625f69ce67f Mon Sep 17 00:00:00 2001 From: Dale Hawkins <107309+dkhawk@users.noreply.github.com> Date: Mon, 29 Jun 2026 22:57:38 +0000 Subject: [PATCH 3/3] docs: document focus traversal behavior and test intent Adds inline comments explaining the Compose and View-level focus configuration on GoogleMap, and KDoc to the focus traversal test class. TAG=agy CONV=c8ea8596-9a4a-488b-885a-df4f8d27c209 --- .../maps/android/compose/GoogleMapFocusTraversalTests.kt | 4 ++++ .../main/java/com/google/maps/android/compose/GoogleMap.kt | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/maps-app/src/androidTest/java/com/google/maps/android/compose/GoogleMapFocusTraversalTests.kt b/maps-app/src/androidTest/java/com/google/maps/android/compose/GoogleMapFocusTraversalTests.kt index fd3a5871..3fa2bb1f 100644 --- a/maps-app/src/androidTest/java/com/google/maps/android/compose/GoogleMapFocusTraversalTests.kt +++ b/maps-app/src/androidTest/java/com/google/maps/android/compose/GoogleMapFocusTraversalTests.kt @@ -29,6 +29,10 @@ import com.google.android.gms.maps.model.LatLng import org.junit.Rule import org.junit.Test +/** + * Verifies keyboard focus traversal behavior for [GoogleMap] when integrated + * inside Compose layouts, ensuring the map behaves as a single focus stop. + */ class GoogleMapFocusTraversalTests { @get:Rule val composeTestRule = createComposeRule() diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 1a567fb1..a4e4d476 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -152,11 +152,17 @@ public fun GoogleMap( val parentCompositionScope = rememberCoroutineScope() AndroidView( + // Make the AndroidView wrapper focusable in Compose so the Compose focus + // system can target it during tab traversal. modifier = modifier.focusable(), factory = { context -> val options = googleMapOptionsFactory() cameraPositionState.isLiteMode = options.liteMode == true mapViewFactory(context, options).also { mapView -> + // Treat the MapView as a single focus stop. FOCUS_BEFORE_DESCENDANTS + // ensures the map container gets focused first, and prevents the keyboard + // focus from tabbing through all internal map elements (like zoom buttons + // or the Google logo) by default. mapView.isFocusable = true mapView.descendantFocusability = ViewGroup.FOCUS_BEFORE_DESCENDANTS