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) {}