diff --git a/stream-chat-android-compose-sample/src/main/AndroidManifest.xml b/stream-chat-android-compose-sample/src/main/AndroidManifest.xml
index 11f4624330d..be45f938777 100644
--- a/stream-chat-android-compose-sample/src/main/AndroidManifest.xml
+++ b/stream-chat-android-compose-sample/src/main/AndroidManifest.xml
@@ -66,54 +66,71 @@
-
-
+
+
Failed to load more files attachments
+ Channel
Open channel info
+
+ Profile
+
diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/info/AddMembersScreen.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/info/AddMembersScreen.kt
index de7958f87ce..329deed70af 100644
--- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/info/AddMembersScreen.kt
+++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/info/AddMembersScreen.kt
@@ -40,6 +40,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.paneTitle
+import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
@@ -101,11 +103,13 @@ internal fun AddMembersScreen(
onDismiss: () -> Unit,
onConfirm: () -> Unit,
) {
+ val paneTitleText = stringResource(id = R.string.stream_compose_add_members_title)
Column(
modifier = Modifier
.fillMaxSize()
.background(ChatTheme.colors.backgroundCoreApp)
- .systemBarsPadding(),
+ .systemBarsPadding()
+ .semantics { paneTitle = paneTitleText },
) {
AddMembersHeader(
hasSelection = state.selectedUserIds.isNotEmpty(),
diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/info/DirectChannelInfoScreen.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/info/DirectChannelInfoScreen.kt
index 834b01f9ef5..7e6db0ae4e8 100644
--- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/info/DirectChannelInfoScreen.kt
+++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/info/DirectChannelInfoScreen.kt
@@ -42,6 +42,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.paneTitle
+import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -113,8 +115,9 @@ private fun DirectChannelInfoScaffold(
onViewAction: (action: ChannelInfoViewAction) -> Unit = {},
) {
val listState = rememberLazyListState()
+ val paneTitleText = stringResource(id = UiCommonR.string.stream_ui_channel_info_contact_title)
Scaffold(
- modifier = modifier,
+ modifier = modifier.semantics { paneTitle = paneTitleText },
topBar = {
ChatTheme.componentFactory.DirectChannelInfoTopBar(
params = DirectChannelInfoTopBarParams(
diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/info/GroupChannelEditScreen.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/info/GroupChannelEditScreen.kt
index 431cded5720..e07642174b4 100644
--- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/info/GroupChannelEditScreen.kt
+++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/info/GroupChannelEditScreen.kt
@@ -54,6 +54,8 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.paneTitle
+import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextOverflow
@@ -206,7 +208,9 @@ private fun GroupChannelEditContent(
onSaveActionClick: () -> Unit = {},
onUploadPictureClick: () -> Unit = {},
) {
+ val paneTitleText = stringResource(id = UiCommonR.string.stream_ui_channel_info_edit_title)
Scaffold(
+ modifier = Modifier.semantics { paneTitle = paneTitleText },
topBar = {
GroupChannelEditTopBar(
isBusy = isBusy,
diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/info/GroupChannelInfoScreen.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/info/GroupChannelInfoScreen.kt
index 84aef692257..4a4f67df612 100644
--- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/info/GroupChannelInfoScreen.kt
+++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/info/GroupChannelInfoScreen.kt
@@ -50,6 +50,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.paneTitle
+import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -174,8 +176,9 @@ private fun GroupChannelInfoScaffold(
onViewAction: (action: ChannelInfoViewAction) -> Unit = {},
) {
val listState = rememberLazyListState()
+ val paneTitleText = stringResource(id = UiCommonR.string.stream_ui_channel_info_group_title)
Scaffold(
- modifier = modifier,
+ modifier = modifier.semantics { paneTitle = paneTitleText },
topBar = {
ChatTheme.componentFactory.GroupChannelInfoTopBar(
params = GroupChannelInfoTopBarParams(
diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/ChannelsScreen.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/ChannelsScreen.kt
index daf295912f3..f0796be33b9 100644
--- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/ChannelsScreen.kt
+++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/ChannelsScreen.kt
@@ -41,6 +41,8 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.paneTitle
+import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import io.getstream.chat.android.compose.state.channels.list.SearchQuery
@@ -67,7 +69,8 @@ import io.getstream.chat.android.ui.common.state.channels.actions.ViewInfo
* You can use the default implementation by not passing in an instance yourself, or you
* can customize the behavior using its parameters.
* @param viewModelKey Key to differentiate between instances of [ChannelListViewModel].
- * @param title Header title.
+ * @param title Header title. Also drives the screen's `paneTitle` semantic, announced by TalkBack
+ * when the screen appears as a pane (e.g. an adaptive-layout pane or a Compose Navigation route).
* @param isShowingHeader If we show the header or hide it.
* @param searchMode The search mode for the screen.
* @param onHeaderActionClick Handler for the default header action.
@@ -123,7 +126,9 @@ public fun ChannelsScreen(
.testTag("Stream_ChannelsScreen"),
) {
Scaffold(
- modifier = Modifier.fillMaxSize(),
+ modifier = Modifier
+ .fillMaxSize()
+ .semantics { paneTitle = title },
topBar = {
if (isShowingHeader) {
ChatTheme.componentFactory.ChannelListHeader(
diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/chats/ChatsScreen.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/chats/ChatsScreen.kt
index d5e1ad9b649..3e3b1b47b23 100644
--- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/chats/ChatsScreen.kt
+++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/chats/ChatsScreen.kt
@@ -44,6 +44,8 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.semantics.paneTitle
+import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.lifecycle.viewmodel.compose.viewModel
@@ -106,6 +108,8 @@ import kotlin.math.abs
* When the initial [ChannelViewModelFactory] is requested (before a channel is selected),
* `channelId`, `messageId`, and `parentMessageId` are `null`.
* @param title The title displayed in the list pane top bar. Default is `"Stream Chat"`.
+ * Also drives the list pane's `paneTitle` semantic, announced by TalkBack when the list pane
+ * appears or becomes active (e.g. switching between panes in the adaptive layout).
* @param searchMode The current search mode. Default is [SearchMode.None].
* @param listContentMode The mode for displaying the list content. Default is [ChatListContentMode.Channels].
* @param onBackPress Callback invoked when the user presses the back button.
@@ -179,10 +183,12 @@ public fun ChatsScreen(
onDispose { navigator.popUpTo(ThreePaneRole.List) }
}
- val listPane = remember(listContentMode) {
+ val listPane = remember(listContentMode, title) {
movableContentOf { modifier: Modifier ->
Scaffold(
- modifier = modifier.safeDrawingPadding(),
+ modifier = modifier
+ .safeDrawingPadding()
+ .semantics { paneTitle = title },
containerColor = ChatTheme.colors.backgroundCoreApp,
topBar = { listTopBarContent() },
bottomBar = { listBottomBarContent() },
diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadsScreen.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadsScreen.kt
index 0992eb5c278..86f8babeb17 100644
--- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadsScreen.kt
+++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadsScreen.kt
@@ -23,6 +23,8 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.paneTitle
+import androidx.compose.ui.semantics.semantics
import androidx.lifecycle.viewmodel.compose.viewModel
import io.getstream.chat.android.client.api.models.QueryThreadsRequest
import io.getstream.chat.android.compose.R
@@ -38,7 +40,8 @@ import io.getstream.chat.android.models.Thread
* It can be used without most parameters for default behavior, that can be tweaked if necessary.
*
* @param viewModelFactory The factory used to build the [ThreadListViewModel].
- * @param title Header title.
+ * @param title Header title. Also drives the screen's `paneTitle` semantic, announced by TalkBack
+ * when the screen appears as a pane (e.g. an adaptive-layout pane or a Compose Navigation route).
* @param onHeaderAvatarClick Handle for when the user clicks on the header avatar.
* @param onThreadClick Handler for Thread item clicks.
*/
@@ -54,7 +57,7 @@ public fun ThreadsScreen(
val user by listViewModel.user.collectAsState()
val connectionState by listViewModel.connectionState.collectAsState()
- Column(modifier = Modifier.fillMaxSize()) {
+ Column(modifier = Modifier.fillMaxSize().semantics { paneTitle = title }) {
ChatTheme.componentFactory.ThreadListHeader(
params = ThreadListHeaderParams(
title = title,
diff --git a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/chats/ChatsScreenTest.kt b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/chats/ChatsScreenTest.kt
index 48a49ebdadb..3f6fc817d4a 100644
--- a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/chats/ChatsScreenTest.kt
+++ b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/chats/ChatsScreenTest.kt
@@ -88,4 +88,16 @@ internal class ChatsScreenTest : MockedChatClientTest {
composeTestRule.onNodeWithTag("Stream_ThreadListLoading")
.assertExists()
}
+
+ @Test
+ @UiThread
+ fun `with custom title`() {
+ composeTestRule.setContent {
+ ChatTheme {
+ ChatsScreen(title = "My Chats")
+ }
+ }
+
+ composeTestRule.onNodeWithText("My Chats").assertExists()
+ }
}
diff --git a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/threads/ThreadsScreenTest.kt b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/threads/ThreadsScreenTest.kt
new file mode 100644
index 00000000000..41554f124a8
--- /dev/null
+++ b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/threads/ThreadsScreenTest.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2014-2026 Stream.io Inc. All rights reserved.
+ *
+ * Licensed under the Stream License;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE
+ *
+ * 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 io.getstream.chat.android.compose.ui.threads
+
+import androidx.annotation.UiThread
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import io.getstream.chat.android.client.test.MockedChatClientTest
+import io.getstream.chat.android.compose.ui.theme.ChatTheme
+import io.getstream.chat.android.models.ConnectionState
+import io.getstream.chat.android.randomUser
+import kotlinx.coroutines.flow.MutableStateFlow
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.whenever
+import org.robolectric.annotation.Config
+
+@RunWith(AndroidJUnit4::class)
+@Config(sdk = [33])
+internal class ThreadsScreenTest : MockedChatClientTest {
+
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ @Before
+ fun prepare() {
+ whenever(mockClientState.user) doReturn MutableStateFlow(randomUser())
+ whenever(mockClientState.connectionState) doReturn MutableStateFlow(ConnectionState.Connected)
+ }
+
+ @Test
+ @UiThread
+ fun `with default title`() {
+ composeTestRule.setContent {
+ ChatTheme {
+ ThreadsScreen()
+ }
+ }
+
+ composeTestRule.onNodeWithText("Threads").assertExists()
+ composeTestRule.onNodeWithTag("Stream_ThreadListLoading").assertExists()
+ }
+
+ @Test
+ @UiThread
+ fun `with custom title`() {
+ composeTestRule.setContent {
+ ChatTheme {
+ ThreadsScreen(title = "My Threads")
+ }
+ }
+
+ composeTestRule.onNodeWithText("My Threads").assertExists()
+ }
+}