Skip to content

Commit 4f69c94

Browse files
committed
library: fix TalkBack double-read and announce item position
1 parent fb3d442 commit 4f69c94

4 files changed

Lines changed: 60 additions & 20 deletions

File tree

example/shared/src/commonMain/kotlin/component/liquid/LiquidGlassNavigationBar.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import androidx.compose.foundation.layout.requiredWidth
2929
import androidx.compose.foundation.layout.size
3030
import androidx.compose.foundation.layout.width
3131
import androidx.compose.foundation.layout.wrapContentWidth
32+
import androidx.compose.foundation.selection.selectableGroup
3233
import androidx.compose.foundation.shape.CircleShape
3334
import androidx.compose.runtime.Composable
3435
import androidx.compose.runtime.CompositionLocalProvider
@@ -58,6 +59,8 @@ import androidx.compose.ui.platform.LocalDensity
5859
import androidx.compose.ui.platform.LocalLayoutDirection
5960
import androidx.compose.ui.semantics.Role
6061
import androidx.compose.ui.semantics.clearAndSetSemantics
62+
import androidx.compose.ui.semantics.selected
63+
import androidx.compose.ui.semantics.semantics
6164
import androidx.compose.ui.text.font.FontWeight
6265
import androidx.compose.ui.text.style.TextOverflow
6366
import androidx.compose.ui.unit.LayoutDirection
@@ -304,6 +307,7 @@ internal fun IosLiquidGlassNavigationBar(
304307
role = Role.Tab,
305308
onClick = { currentIndex = index },
306309
)
310+
.semantics { selected = index == currentIndex }
307311
.weight(1f)
308312
.fillMaxHeight()
309313
.graphicsLayer {
@@ -317,7 +321,8 @@ internal fun IosLiquidGlassNavigationBar(
317321
Icon(
318322
modifier = Modifier.size(22.dp),
319323
imageVector = item.icon,
320-
contentDescription = item.label,
324+
// Decorative: the adjacent label names the item; avoids TalkBack double-read.
325+
contentDescription = null,
321326
)
322327
Text(
323328
text = item.label,
@@ -340,6 +345,7 @@ internal fun IosLiquidGlassNavigationBar(
340345
CompositionLocalProvider(LocalContentColor provides tabContentColor) {
341346
Row(
342347
modifier = Modifier
348+
.selectableGroup()
343349
.onSizeChanged { coords ->
344350
totalWidthPx = coords.width.toFloat()
345351
val contentWidthPx = totalWidthPx - with(density) { 8.dp.toPx() }

miuix-ui/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/NavigationBar.kt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import androidx.compose.foundation.layout.size
3030
import androidx.compose.foundation.layout.statusBars
3131
import androidx.compose.foundation.layout.windowInsetsPadding
3232
import androidx.compose.foundation.selection.selectable
33+
import androidx.compose.foundation.selection.selectableGroup
3334
import androidx.compose.foundation.shape.RoundedCornerShape
3435
import androidx.compose.runtime.Composable
3536
import androidx.compose.runtime.CompositionLocalProvider
@@ -96,7 +97,9 @@ fun NavigationBar(
9697
HorizontalDivider()
9798
}
9899
Row(
99-
modifier = Modifier.fillMaxWidth(),
100+
modifier = Modifier
101+
.fillMaxWidth()
102+
.selectableGroup(),
100103
horizontalArrangement = Arrangement.SpaceBetween,
101104
verticalAlignment = Alignment.CenterVertically,
102105
) {
@@ -185,7 +188,8 @@ fun RowScope.NavigationBarItem(
185188
Image(
186189
modifier = Modifier.padding(top = NavigationBarDefaults.IconTopPadding).size(NavigationBarDefaults.IconSize),
187190
imageVector = icon,
188-
contentDescription = label,
191+
// Decorative: the adjacent label already names the item; avoids TalkBack double-read.
192+
contentDescription = null,
189193
colorFilter = ColorFilter.tint(tint),
190194
)
191195
Text(
@@ -222,7 +226,8 @@ fun RowScope.NavigationBarItem(
222226
}
223227
.size(NavigationBarDefaults.IconSize),
224228
imageVector = icon,
225-
contentDescription = label,
229+
// Decorative: the label (always present in the tree) names the item; avoids double-read.
230+
contentDescription = null,
226231
colorFilter = ColorFilter.tint(tint),
227232
)
228233
Text(
@@ -307,6 +312,7 @@ fun FloatingNavigationBar(
307312
) {
308313
Row(
309314
modifier = Modifier
315+
.selectableGroup()
310316
.padding(bottom = bottomPaddingValue)
311317
.defaultMinSize(minHeight = 52.dp)
312318
.then(

miuix-ui/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/NavigationRail.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import androidx.compose.foundation.layout.width
2828
import androidx.compose.foundation.layout.windowInsetsPadding
2929
import androidx.compose.foundation.rememberScrollState
3030
import androidx.compose.foundation.selection.selectable
31+
import androidx.compose.foundation.selection.selectableGroup
3132
import androidx.compose.foundation.verticalScroll
3233
import androidx.compose.runtime.Composable
3334
import androidx.compose.runtime.CompositionLocalProvider
@@ -89,6 +90,7 @@ fun NavigationRail(
8990
.width(minWidth)
9091
.fillMaxHeight()
9192
.verticalScroll(rememberScrollState())
93+
.selectableGroup()
9294
.padding(vertical = NavigationRailDefaults.VerticalPadding),
9395
horizontalAlignment = Alignment.CenterHorizontally,
9496
verticalArrangement = Arrangement.Top,
@@ -165,7 +167,8 @@ fun NavigationRailItem(
165167
Image(
166168
modifier = Modifier.size(NavigationRailDefaults.IconSize),
167169
imageVector = icon,
168-
contentDescription = label,
170+
// Decorative: the adjacent label already names the item; avoids TalkBack double-read.
171+
contentDescription = null,
169172
colorFilter = ColorFilter.tint(tint),
170173
)
171174
Spacer(modifier = Modifier.height(NavigationRailDefaults.IconTextSpacing))
@@ -182,7 +185,8 @@ fun NavigationRailItem(
182185
Image(
183186
modifier = Modifier.size(NavigationRailDefaults.IconSize),
184187
imageVector = icon,
185-
contentDescription = label,
188+
// The label only exists in the tree when selected; name the icon otherwise to avoid double-read.
189+
contentDescription = if (selected) null else label,
186190
colorFilter = ColorFilter.tint(tint),
187191
)
188192
if (selected) {

miuix-ui/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/TabRow.kt

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import androidx.compose.animation.core.LinearEasing
88
import androidx.compose.animation.core.tween
99
import androidx.compose.foundation.Indication
1010
import androidx.compose.foundation.background
11-
import androidx.compose.foundation.clickable
1211
import androidx.compose.foundation.interaction.MutableInteractionSource
1312
import androidx.compose.foundation.layout.Arrangement
1413
import androidx.compose.foundation.layout.Box
@@ -24,6 +23,7 @@ import androidx.compose.foundation.lazy.LazyListState
2423
import androidx.compose.foundation.lazy.LazyRow
2524
import androidx.compose.foundation.lazy.itemsIndexed
2625
import androidx.compose.foundation.lazy.rememberLazyListState
26+
import androidx.compose.foundation.selection.selectable
2727
import androidx.compose.runtime.Composable
2828
import androidx.compose.runtime.Immutable
2929
import androidx.compose.runtime.LaunchedEffect
@@ -38,9 +38,11 @@ import androidx.compose.ui.Alignment
3838
import androidx.compose.ui.Modifier
3939
import androidx.compose.ui.graphics.Color
4040
import androidx.compose.ui.platform.LocalDensity
41+
import androidx.compose.ui.semantics.CollectionInfo
42+
import androidx.compose.ui.semantics.CollectionItemInfo
4143
import androidx.compose.ui.semantics.Role
42-
import androidx.compose.ui.semantics.role
43-
import androidx.compose.ui.semantics.selected
44+
import androidx.compose.ui.semantics.collectionInfo
45+
import androidx.compose.ui.semantics.collectionItemInfo
4446
import androidx.compose.ui.semantics.semantics
4547
import androidx.compose.ui.text.font.FontWeight
4648
import androidx.compose.ui.text.style.TextOverflow
@@ -154,14 +156,18 @@ fun TabRow(
154156
LazyRow(
155157
state = config.listState,
156158
modifier = Modifier
157-
.fillMaxSize(),
159+
.fillMaxSize()
160+
// Announce "tab X of Y": this explicit count overrides LazyRow's auto-derived
161+
// collectionInfo (the leftmost semantics in the chain wins the property).
162+
.semantics { collectionInfo = CollectionInfo(rowCount = 1, columnCount = tabs.size) },
158163
verticalAlignment = Alignment.CenterVertically,
159164
horizontalArrangement = Arrangement.spacedBy(itemSpacing),
160165
overscrollEffect = null,
161166
) {
162167
itemsIndexed(tabs) { index, tabText ->
163168
TabItem(
164169
text = tabText,
170+
index = index,
165171
isSelected = selectedTabIndex == index,
166172
onClick = { currentOnTabSelected.invoke(index) },
167173
cornerRadius = config.cornerRadius,
@@ -284,14 +290,18 @@ fun TabRowWithContour(
284290
LazyRow(
285291
state = config.listState,
286292
modifier = Modifier
287-
.fillMaxSize(),
293+
.fillMaxSize()
294+
// Announce "tab X of Y": this explicit count overrides LazyRow's auto-derived
295+
// collectionInfo (the leftmost semantics in the chain wins the property).
296+
.semantics { collectionInfo = CollectionInfo(rowCount = 1, columnCount = tabs.size) },
288297
verticalAlignment = Alignment.CenterVertically,
289298
horizontalArrangement = Arrangement.spacedBy(itemSpacing),
290299
overscrollEffect = null,
291300
) {
292301
itemsIndexed(tabs) { index, tabText ->
293302
TabItemWithContour(
294303
text = tabText,
304+
index = index,
295305
isSelected = selectedTabIndex == index,
296306
onClick = { currentOnTabSelected.invoke(index) },
297307
cornerRadius = config.cornerRadius,
@@ -310,6 +320,7 @@ fun TabRowWithContour(
310320
@Composable
311321
private fun TabItem(
312322
text: String,
323+
index: Int,
313324
isSelected: Boolean,
314325
onClick: () -> Unit,
315326
cornerRadius: Dp,
@@ -324,14 +335,20 @@ private fun TabItem(
324335
.fillMaxHeight()
325336
.width(width)
326337
.squircleClip(cornerRadius)
327-
.clickable(
338+
.selectable(
339+
selected = isSelected,
340+
onClick = onClick,
341+
role = Role.Tab,
328342
interactionSource = interactionSource,
329343
indication = indication,
330-
onClick = onClick,
331344
)
332345
.semantics {
333-
role = Role.Tab
334-
selected = isSelected
346+
collectionItemInfo = CollectionItemInfo(
347+
rowIndex = 0,
348+
rowSpan = 1,
349+
columnIndex = index,
350+
columnSpan = 1,
351+
)
335352
}
336353
.padding(horizontal = 12.dp),
337354
contentAlignment = contentAlignment,
@@ -350,6 +367,7 @@ private fun TabItem(
350367
@Composable
351368
private fun TabItemWithContour(
352369
text: String,
370+
index: Int,
353371
isSelected: Boolean,
354372
onClick: () -> Unit,
355373
cornerRadius: Dp,
@@ -364,14 +382,20 @@ private fun TabItemWithContour(
364382
.fillMaxHeight()
365383
.width(width)
366384
.squircleClip(cornerRadius)
367-
.clickable(
385+
.selectable(
386+
selected = isSelected,
387+
onClick = onClick,
388+
role = Role.Tab,
368389
interactionSource = interactionSource,
369390
indication = indication,
370-
onClick = onClick,
371391
)
372392
.semantics {
373-
role = Role.Tab
374-
selected = isSelected
393+
collectionItemInfo = CollectionItemInfo(
394+
rowIndex = 0,
395+
rowSpan = 1,
396+
columnIndex = index,
397+
columnSpan = 1,
398+
)
375399
},
376400
contentAlignment = contentAlignment,
377401
) {
@@ -392,7 +416,7 @@ private fun TabItemWithContour(
392416
private data class TabRowConfig(
393417
val tabWidth: Dp,
394418
val cornerRadius: Dp,
395-
val listState: androidx.compose.foundation.lazy.LazyListState,
419+
val listState: LazyListState,
396420
)
397421

398422
/**

0 commit comments

Comments
 (0)