Skip to content

Commit a6d440c

Browse files
andremiongpunto
authored andcommitted
Improve compose TalkBack screen-entry announcements (#6467)
* Add android:label to compose sample activities for TalkBack TalkBack announces android:label on Activity entry. Without per-activity labels, every screen was announced as 'Chat Sample Compose'. Each non-routing activity now has a screen-specific label, reusing existing sample/SDK strings where they fit; only 'channel_title' and 'user_profile_title' are new keys. * Add paneTitle to compose SDK screens for TalkBack pane announcements paneTitle is the Compose semantic that drives TalkBack's pane-change announcement (TYPE_WINDOW_STATE_CHANGED) when a screen appears via FullscreenDialog, adaptive-layout pane swap, or Compose Navigation route change inside an existing Activity. Without it, TalkBack only announces the host Activity's android:label on entry and stays silent on subsequent in-Activity pane changes. - AddMembersScreen, GroupChannelEditScreen: announce 'Add Members' / 'Edit' when shown via FullscreenDialog from GroupChannelInfoScreen. - DirectChannelInfoScreen, GroupChannelInfoScreen, ChannelsScreen, ChatsScreen, ThreadsScreen: announce their title when used as a pane in an adaptive layout or routed-to via Compose Navigation. All reuse existing strings or the screen's existing title parameter; no new public API. * Document paneTitle behavior on title @param of public screen composables ChannelsScreen, ChatsScreen, and ThreadsScreen route their title parameter into Modifier.semantics { paneTitle = ... } on the screen root. The @param title KDoc now states that contract so integrators know the value is announced by TalkBack when the screen appears as a pane.
1 parent 7487ed7 commit a6d440c

11 files changed

Lines changed: 146 additions & 11 deletions

File tree

stream-chat-android-compose-sample/src/main/AndroidManifest.xml

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,54 +66,71 @@
6666
<activity
6767
android:name=".ui.login.UserLoginActivity"
6868
android:exported="false"
69+
android:label="@string/user_login_screen_title"
6970
/>
7071
<activity
7172
android:name=".ui.login.CustomLoginActivity"
7273
android:exported="false"
74+
android:label="@string/user_login_advanced_options"
7375
android:windowSoftInputMode="adjustResize"
7476
/>
7577
<activity
7678
android:name=".ui.chats.ChatsActivity"
7779
android:exported="false"
80+
android:label="@string/app_bottom_bar_chats"
7881
/>
7982
<activity
8083
android:name=".feature.channel.list.ChannelsActivity"
8184
android:exported="false"
85+
android:label="@string/app_bottom_bar_chats"
8286
/>
8387
<activity
8488
android:name=".ui.channel.ChannelActivity"
8589
android:exported="false"
90+
android:label="@string/channel_title"
8691
android:windowSoftInputMode="adjustResize"
8792
/>
8893
<activity
8994
android:name=".ui.channel.DirectChannelInfoActivity"
9095
android:exported="false"
96+
android:label="@string/stream_ui_channel_info_contact_title"
9197
/>
9298
<activity
9399
android:name=".ui.channel.GroupChannelInfoActivity"
94100
android:exported="false"
101+
android:label="@string/stream_ui_channel_info_group_title"
95102
/>
96103
<activity
97104
android:name=".ui.pinned.PinnedMessagesActivity"
98105
android:exported="false"
106+
android:label="@string/pinned_messages_title"
99107
/>
100108
<activity
101109
android:name=".feature.channel.add.AddChannelActivity"
102110
android:exported="false"
111+
android:label="@string/add_channel_title"
103112
android:windowSoftInputMode="adjustResize"
104113
/>
105114
<activity
106115
android:name=".feature.channel.add.group.AddGroupChannelActivity"
107116
android:exported="false"
117+
android:label="@string/add_group_channel_members_title"
108118
android:windowSoftInputMode="adjustResize"
109119
/>
110120
<activity
111121
android:name=".feature.channel.draft.DraftChannelActivity"
112122
android:exported="false"
123+
android:label="@string/stream_compose_channel_list_header_new_chat"
113124
android:windowSoftInputMode="adjustResize"
114125
/>
115-
<activity android:name=".feature.reminders.MessageRemindersActivity" />
116-
<activity android:name=".ui.profile.UserProfileActivity" />
126+
<activity
127+
android:name=".feature.reminders.MessageRemindersActivity"
128+
android:label="@string/reminders_title"
129+
/>
130+
<activity
131+
android:name=".ui.profile.UserProfileActivity"
132+
android:label="@string/user_profile_title"
133+
/>
117134
<activity
118135
android:name=".ui.channel.attachments.ChannelFilesAttachmentsActivity"
119136
android:label="@string/stream_ui_channel_attachments_files_title"

stream-chat-android-compose-sample/src/main/res/values/strings.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,10 @@
128128
<string name="channel_attachments_files_loading_more_error">Failed to load more files attachments</string>
129129

130130
<!-- Channel -->
131+
<string name="channel_title">Channel</string>
131132
<string name="channel_open_info">Open channel info</string>
132133

134+
<!-- User Profile -->
135+
<string name="user_profile_title">Profile</string>
136+
133137
</resources>

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/info/AddMembersScreen.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ import androidx.compose.ui.Alignment
4040
import androidx.compose.ui.Modifier
4141
import androidx.compose.ui.res.painterResource
4242
import androidx.compose.ui.res.stringResource
43+
import androidx.compose.ui.semantics.paneTitle
44+
import androidx.compose.ui.semantics.semantics
4345
import androidx.compose.ui.text.style.TextAlign
4446
import androidx.compose.ui.text.style.TextOverflow
4547
import androidx.compose.ui.tooling.preview.Preview
@@ -101,11 +103,13 @@ internal fun AddMembersScreen(
101103
onDismiss: () -> Unit,
102104
onConfirm: () -> Unit,
103105
) {
106+
val paneTitleText = stringResource(id = R.string.stream_compose_add_members_title)
104107
Column(
105108
modifier = Modifier
106109
.fillMaxSize()
107110
.background(ChatTheme.colors.backgroundCoreApp)
108-
.systemBarsPadding(),
111+
.systemBarsPadding()
112+
.semantics { paneTitle = paneTitleText },
109113
) {
110114
AddMembersHeader(
111115
hasSelection = state.selectedUserIds.isNotEmpty(),

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/info/DirectChannelInfoScreen.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ import androidx.compose.ui.Alignment
4242
import androidx.compose.ui.Modifier
4343
import androidx.compose.ui.platform.LocalContext
4444
import androidx.compose.ui.res.stringResource
45+
import androidx.compose.ui.semantics.paneTitle
46+
import androidx.compose.ui.semantics.semantics
4547
import androidx.compose.ui.text.style.TextOverflow
4648
import androidx.compose.ui.tooling.preview.Preview
4749
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -113,8 +115,9 @@ private fun DirectChannelInfoScaffold(
113115
onViewAction: (action: ChannelInfoViewAction) -> Unit = {},
114116
) {
115117
val listState = rememberLazyListState()
118+
val paneTitleText = stringResource(id = UiCommonR.string.stream_ui_channel_info_contact_title)
116119
Scaffold(
117-
modifier = modifier,
120+
modifier = modifier.semantics { paneTitle = paneTitleText },
118121
topBar = {
119122
ChatTheme.componentFactory.DirectChannelInfoTopBar(
120123
params = DirectChannelInfoTopBarParams(

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/info/GroupChannelEditScreen.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ import androidx.compose.ui.platform.LocalContext
5454
import androidx.compose.ui.platform.LocalInspectionMode
5555
import androidx.compose.ui.res.painterResource
5656
import androidx.compose.ui.res.stringResource
57+
import androidx.compose.ui.semantics.paneTitle
58+
import androidx.compose.ui.semantics.semantics
5759
import androidx.compose.ui.text.TextRange
5860
import androidx.compose.ui.text.input.TextFieldValue
5961
import androidx.compose.ui.text.style.TextOverflow
@@ -206,7 +208,9 @@ private fun GroupChannelEditContent(
206208
onSaveActionClick: () -> Unit = {},
207209
onUploadPictureClick: () -> Unit = {},
208210
) {
211+
val paneTitleText = stringResource(id = UiCommonR.string.stream_ui_channel_info_edit_title)
209212
Scaffold(
213+
modifier = Modifier.semantics { paneTitle = paneTitleText },
210214
topBar = {
211215
GroupChannelEditTopBar(
212216
isBusy = isBusy,

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channel/info/GroupChannelInfoScreen.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ import androidx.compose.ui.Modifier
5050
import androidx.compose.ui.platform.LocalContext
5151
import androidx.compose.ui.res.pluralStringResource
5252
import androidx.compose.ui.res.stringResource
53+
import androidx.compose.ui.semantics.paneTitle
54+
import androidx.compose.ui.semantics.semantics
5355
import androidx.compose.ui.text.style.TextOverflow
5456
import androidx.compose.ui.tooling.preview.Preview
5557
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -174,8 +176,9 @@ private fun GroupChannelInfoScaffold(
174176
onViewAction: (action: ChannelInfoViewAction) -> Unit = {},
175177
) {
176178
val listState = rememberLazyListState()
179+
val paneTitleText = stringResource(id = UiCommonR.string.stream_ui_channel_info_group_title)
177180
Scaffold(
178-
modifier = modifier,
181+
modifier = modifier.semantics { paneTitle = paneTitleText },
179182
topBar = {
180183
ChatTheme.componentFactory.GroupChannelInfoTopBar(
181184
params = GroupChannelInfoTopBarParams(

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/channels/ChannelsScreen.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ import androidx.compose.runtime.setValue
4141
import androidx.compose.ui.Alignment
4242
import androidx.compose.ui.Modifier
4343
import androidx.compose.ui.platform.testTag
44+
import androidx.compose.ui.semantics.paneTitle
45+
import androidx.compose.ui.semantics.semantics
4446
import androidx.compose.ui.unit.dp
4547
import androidx.lifecycle.viewmodel.compose.viewModel
4648
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
6769
* You can use the default implementation by not passing in an instance yourself, or you
6870
* can customize the behavior using its parameters.
6971
* @param viewModelKey Key to differentiate between instances of [ChannelListViewModel].
70-
* @param title Header title.
72+
* @param title Header title. Also drives the screen's `paneTitle` semantic, announced by TalkBack
73+
* when the screen appears as a pane (e.g. an adaptive-layout pane or a Compose Navigation route).
7174
* @param isShowingHeader If we show the header or hide it.
7275
* @param searchMode The search mode for the screen.
7376
* @param onHeaderActionClick Handler for the default header action.
@@ -123,7 +126,9 @@ public fun ChannelsScreen(
123126
.testTag("Stream_ChannelsScreen"),
124127
) {
125128
Scaffold(
126-
modifier = Modifier.fillMaxSize(),
129+
modifier = Modifier
130+
.fillMaxSize()
131+
.semantics { paneTitle = title },
127132
topBar = {
128133
if (isShowingHeader) {
129134
ChatTheme.componentFactory.ChannelListHeader(

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/chats/ChatsScreen.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ import androidx.compose.runtime.setValue
4444
import androidx.compose.ui.Modifier
4545
import androidx.compose.ui.layout.onSizeChanged
4646
import androidx.compose.ui.platform.LocalContext
47+
import androidx.compose.ui.semantics.paneTitle
48+
import androidx.compose.ui.semantics.semantics
4749
import androidx.compose.ui.unit.IntOffset
4850
import androidx.compose.ui.unit.IntSize
4951
import androidx.lifecycle.viewmodel.compose.viewModel
@@ -106,6 +108,8 @@ import kotlin.math.abs
106108
* When the initial [ChannelViewModelFactory] is requested (before a channel is selected),
107109
* `channelId`, `messageId`, and `parentMessageId` are `null`.
108110
* @param title The title displayed in the list pane top bar. Default is `"Stream Chat"`.
111+
* Also drives the list pane's `paneTitle` semantic, announced by TalkBack when the list pane
112+
* appears or becomes active (e.g. switching between panes in the adaptive layout).
109113
* @param searchMode The current search mode. Default is [SearchMode.None].
110114
* @param listContentMode The mode for displaying the list content. Default is [ChatListContentMode.Channels].
111115
* @param onBackPress Callback invoked when the user presses the back button.
@@ -179,10 +183,12 @@ public fun ChatsScreen(
179183
onDispose { navigator.popUpTo(ThreePaneRole.List) }
180184
}
181185

182-
val listPane = remember(listContentMode) {
186+
val listPane = remember(listContentMode, title) {
183187
movableContentOf { modifier: Modifier ->
184188
Scaffold(
185-
modifier = modifier.safeDrawingPadding(),
189+
modifier = modifier
190+
.safeDrawingPadding()
191+
.semantics { paneTitle = title },
186192
containerColor = ChatTheme.colors.backgroundCoreApp,
187193
topBar = { listTopBarContent() },
188194
bottomBar = { listBottomBarContent() },

stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/threads/ThreadsScreen.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import androidx.compose.runtime.collectAsState
2323
import androidx.compose.runtime.getValue
2424
import androidx.compose.ui.Modifier
2525
import androidx.compose.ui.res.stringResource
26+
import androidx.compose.ui.semantics.paneTitle
27+
import androidx.compose.ui.semantics.semantics
2628
import androidx.lifecycle.viewmodel.compose.viewModel
2729
import io.getstream.chat.android.client.api.models.QueryThreadsRequest
2830
import io.getstream.chat.android.compose.R
@@ -38,7 +40,8 @@ import io.getstream.chat.android.models.Thread
3840
* It can be used without most parameters for default behavior, that can be tweaked if necessary.
3941
*
4042
* @param viewModelFactory The factory used to build the [ThreadListViewModel].
41-
* @param title Header title.
43+
* @param title Header title. Also drives the screen's `paneTitle` semantic, announced by TalkBack
44+
* when the screen appears as a pane (e.g. an adaptive-layout pane or a Compose Navigation route).
4245
* @param onHeaderAvatarClick Handle for when the user clicks on the header avatar.
4346
* @param onThreadClick Handler for Thread item clicks.
4447
*/
@@ -54,7 +57,7 @@ public fun ThreadsScreen(
5457
val user by listViewModel.user.collectAsState()
5558
val connectionState by listViewModel.connectionState.collectAsState()
5659

57-
Column(modifier = Modifier.fillMaxSize()) {
60+
Column(modifier = Modifier.fillMaxSize().semantics { paneTitle = title }) {
5861
ChatTheme.componentFactory.ThreadListHeader(
5962
params = ThreadListHeaderParams(
6063
title = title,

stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/chats/ChatsScreenTest.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,4 +88,16 @@ internal class ChatsScreenTest : MockedChatClientTest {
8888
composeTestRule.onNodeWithTag("Stream_ThreadListLoading")
8989
.assertExists()
9090
}
91+
92+
@Test
93+
@UiThread
94+
fun `with custom title`() {
95+
composeTestRule.setContent {
96+
ChatTheme {
97+
ChatsScreen(title = "My Chats")
98+
}
99+
}
100+
101+
composeTestRule.onNodeWithText("My Chats").assertExists()
102+
}
91103
}

0 commit comments

Comments
 (0)