Add a11y handling for Carousel and update samples in playground app
It includes:
- Updated a11y behaviour for Carousel & CarouselItem
- Added a11y samples for LazyList, ImmersiveList, Carousel
& SideNavigation in the playground app
Test: N/A
Change-Id: If5903acaeda99e61c46d56d7819b0926add557cc
diff --git a/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/Card.kt b/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/Card.kt
index c517d99..332cf06 100644
--- a/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/Card.kt
+++ b/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/Card.kt
@@ -17,7 +17,7 @@
package androidx.tv.integration.playground
import androidx.compose.foundation.background
-import androidx.compose.foundation.focusable
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
@@ -37,6 +37,6 @@
.width(200.dp)
.height(150.dp)
.drawBorderOnFocus()
- .focusable()
+ .clickable { }
)
}
\ No newline at end of file
diff --git a/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/FeaturedCarousel.kt b/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/FeaturedCarousel.kt
index 64f92b9..705ef91 100644
--- a/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/FeaturedCarousel.kt
+++ b/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/FeaturedCarousel.kt
@@ -16,7 +16,6 @@
package androidx.tv.integration.playground
-import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.focusable
@@ -42,8 +41,9 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
-import androidx.tv.foundation.ExperimentalTvFoundationApi
import androidx.tv.material3.Carousel
import androidx.tv.material3.CarouselDefaults
import androidx.tv.material3.CarouselState
@@ -88,9 +88,7 @@
}
}
-@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalAnimationApi::class,
- ExperimentalTvFoundationApi::class
-)
+@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
internal fun FeaturedCarousel(modifier: Modifier = Modifier) {
val backgrounds = listOf(
@@ -105,41 +103,47 @@
)
val carouselState = remember { CarouselState() }
- FocusGroup {
- Carousel(
- itemCount = backgrounds.size,
- carouselState = carouselState,
- modifier = modifier
- .height(300.dp)
- .fillMaxWidth(),
- carouselIndicator = {
- CarouselDefaults.IndicatorRow(
- itemCount = backgrounds.size,
- activeItemIndex = carouselState.activeItemIndex,
+ Carousel(
+ itemCount = backgrounds.size,
+ carouselState = carouselState,
+ modifier = modifier
+ .height(300.dp)
+ .fillMaxWidth(),
+ carouselIndicator = {
+ CarouselDefaults.IndicatorRow(
+ itemCount = backgrounds.size,
+ activeItemIndex = carouselState.activeItemIndex,
+ modifier = Modifier
+ .align(Alignment.BottomEnd)
+ .padding(16.dp),
+ )
+ }
+ ) { itemIndex ->
+ CarouselItem(
+ modifier = Modifier.semantics {
+ contentDescription = "Featured Content"
+ },
+ background = {
+ Box(
modifier = Modifier
- .align(Alignment.BottomEnd)
- .padding(16.dp),
+ .background(backgrounds[itemIndex])
+ .fillMaxSize()
)
- }
- ) { itemIndex ->
- CarouselItem(
- background = {
- Box(
- modifier = Modifier
- .background(backgrounds[itemIndex])
- .fillMaxSize()
- )
- },
- modifier =
- if (itemIndex == 0)
- Modifier.initiallyFocused()
- else
- Modifier.restorableFocus()
+ },
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(20.dp),
+ contentAlignment = Alignment.BottomStart
) {
- Box(modifier = Modifier) {
- OverlayButton(
- modifier = Modifier
- )
+ Column {
+ Text(text = "This is sample text content.", color = Color.Yellow)
+ Text(text = "Sample description.", color = Color.Yellow)
+ Row {
+ OverlayButton(text = "Play")
+ OverlayButton(text = "Add to Watchlist")
+ }
}
}
}
@@ -147,14 +151,16 @@
}
@Composable
-private fun OverlayButton(modifier: Modifier = Modifier) {
+private fun OverlayButton(modifier: Modifier = Modifier, text: String = "Test Button") {
var isFocused by remember { mutableStateOf(false) }
Button(
onClick = { },
modifier = modifier
- .onFocusChanged { isFocused = it.isFocused }
- .padding(40.dp)
+ .onFocusChanged {
+ isFocused = it.isFocused
+ }
+ .padding(20.dp)
.border(
width = 2.dp,
color = if (isFocused) Color.Red else Color.Transparent,
@@ -162,6 +168,6 @@
)
.padding(vertical = 2.dp, horizontal = 5.dp)
) {
- Text(text = "Play")
+ Text(text = text)
}
}
diff --git a/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/ImmersiveList.kt b/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/ImmersiveList.kt
index b8ed37f..ac3d021 100644
--- a/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/ImmersiveList.kt
+++ b/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/ImmersiveList.kt
@@ -16,24 +16,19 @@
package androidx.tv.integration.playground
-import android.util.Log
import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.semantics.CollectionItemInfo
+import androidx.compose.ui.semantics.collectionItemInfo
+import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import androidx.tv.foundation.ExperimentalTvFoundationApi
import androidx.tv.foundation.lazy.list.TvLazyColumn
@@ -54,7 +49,6 @@
private fun SampleImmersiveList() {
val immersiveListHeight = 300.dp
val cardSpacing = 10.dp
- val cardWidth = 200.dp
val cardHeight = 150.dp
val backgrounds = listOf(
Color.Red,
@@ -76,27 +70,24 @@
)
}
) {
- Row(horizontalArrangement = Arrangement.spacedBy(cardSpacing)) {
- backgrounds.forEachIndexed { index, backgroundColor ->
- var isFocused by remember { mutableStateOf(false) }
+ LazyRow(
+ horizontalArrangement = Arrangement.spacedBy(cardSpacing),
+ modifier = Modifier.lazyListSemantics(1, backgrounds.count())
+ ) {
+ itemsIndexed(backgrounds) { index, backgroundColor ->
+ val cardModifier =
+ if (index == 0)
+ Modifier.initiallyFocused()
+ else
+ Modifier.restorableFocus()
- Box(
- modifier = Modifier
- .background(backgroundColor)
- .width(cardWidth)
- .height(cardHeight)
- .border(5.dp, Color.White.copy(alpha = if (isFocused) 1f else 0.3f))
- .then(
- if (index == 0)
- Modifier.initiallyFocused()
- else
- Modifier.restorableFocus()
- )
- .onFocusChanged { isFocused = it.isFocused }
- .immersiveListItem(index)
- .clickable {
- Log.d("ImmersiveList", "Item $index was clicked")
+ Card(
+ modifier = cardModifier
+ .semantics {
+ collectionItemInfo = CollectionItemInfo(0, 1, index, 1)
}
+ .immersiveListItem(index),
+ backgroundColor = backgroundColor
)
}
}
diff --git a/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/LazyRowsAndColumns.kt b/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/LazyRowsAndColumns.kt
index 7ecfec8..2fbddf4 100644
--- a/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/LazyRowsAndColumns.kt
+++ b/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/LazyRowsAndColumns.kt
@@ -25,11 +25,17 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.semantics.CollectionInfo
+import androidx.compose.ui.semantics.CollectionItemInfo
+import androidx.compose.ui.semantics.collectionInfo
+import androidx.compose.ui.semantics.collectionItemInfo
+import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import androidx.tv.foundation.ExperimentalTvFoundationApi
import androidx.tv.foundation.PivotOffsets
import androidx.tv.foundation.lazy.list.TvLazyColumn
import androidx.tv.foundation.lazy.list.TvLazyRow
+import androidx.tv.foundation.lazy.list.itemsIndexed
const val rowsCount = 20
const val columnsCount = 100
@@ -58,19 +64,35 @@
val backgroundColors = List(columnsCount) { colors.random() }
FocusGroup {
- TvLazyRow(modifier, horizontalArrangement = Arrangement.spacedBy(10.dp)) {
- backgroundColors.forEachIndexed { index, backgroundColor ->
- item {
- Card(
- backgroundColor = backgroundColor,
- modifier =
- if (index == 0)
- Modifier.initiallyFocused()
- else
- Modifier.restorableFocus()
- )
- }
+ TvLazyRow(
+ modifier = modifier.lazyListSemantics(1, columnsCount),
+ horizontalArrangement = Arrangement.spacedBy(10.dp)
+ ) {
+ itemsIndexed(backgroundColors) { index, item ->
+ val cardModifier =
+ if (index == 0)
+ Modifier.initiallyFocused()
+ else
+ Modifier.restorableFocus()
+
+ Card(
+ modifier = cardModifier.semantics {
+ collectionItemInfo = CollectionItemInfo(0, 1, index, 1)
+ },
+ backgroundColor = item
+ )
}
}
}
}
+
+@Composable
+fun Modifier.lazyListSemantics(rowCount: Int = -1, columnCount: Int = -1): Modifier {
+ return this.then(
+ remember(rowCount, columnCount) {
+ Modifier.semantics {
+ collectionInfo = CollectionInfo(rowCount, columnCount)
+ }
+ }
+ )
+}
diff --git a/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/NavigationDrawer.kt b/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/NavigationDrawer.kt
index 86865b3..1257c1b 100644
--- a/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/NavigationDrawer.kt
+++ b/tv/integration-tests/playground/src/main/java/androidx/tv/integration/playground/NavigationDrawer.kt
@@ -18,6 +18,7 @@
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -28,11 +29,11 @@
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.selection.selectableGroup
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowLeft
import androidx.compose.material.icons.filled.KeyboardArrowRight
-import androidx.compose.material3.Button
-import androidx.compose.material3.ButtonDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
@@ -43,14 +44,16 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.semantics.selected
+import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
-import androidx.tv.foundation.ExperimentalTvFoundationApi
import androidx.tv.material3.DrawerValue
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Icon
@@ -110,7 +113,7 @@
}
}
-@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalTvFoundationApi::class)
+@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
private fun Sidebar(
drawerValue: DrawerValue,
@@ -128,8 +131,10 @@
Column(
modifier = Modifier
.fillMaxHeight()
- .background(pageColor),
+ .background(pageColor)
+ .selectableGroup(),
horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(10.dp)
) {
NavigationItem(
imageVector = Icons.Default.KeyboardArrowRight,
@@ -160,15 +165,19 @@
) {
var isFocused by remember { mutableStateOf(false) }
- Button(
- onClick = { selectedIndex.value = index },
+ Box(
modifier = modifier
- .onFocusChanged { isFocused = it.isFocused },
- colors = ButtonDefaults.filledTonalButtonColors(
- containerColor = if (isFocused) Color.White else Color.Transparent,
- )
+ .clip(RoundedCornerShape(10.dp))
+ .onFocusChanged { isFocused = it.isFocused }
+ .background(if (isFocused) Color.White else Color.Transparent)
+ .semantics(mergeDescendants = true) {
+ selected = selectedIndex.value == index
+ }
+ .clickable {
+ selectedIndex.value = index
+ }
) {
- Box(modifier = Modifier) {
+ Box(modifier = Modifier.padding(10.dp)) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(5.dp),
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselScopeTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselScopeTest.kt
index 10080e0..2f80311 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselScopeTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselScopeTest.kt
@@ -16,7 +16,6 @@
package androidx.tv.material3
-import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.focusable
@@ -50,7 +49,7 @@
@get:Rule
val rule = createComposeRule()
- @OptIn(ExperimentalTvMaterial3Api::class, ExperimentalAnimationApi::class)
+ @OptIn(ExperimentalTvMaterial3Api::class)
@Test
fun carouselItem_parentContainerGainsFocused_onBackPress() {
val containerBoxTag = "container-box"
@@ -69,7 +68,8 @@
) {
CarouselScope(carouselState = carouselState)
.CarouselItem(
- modifier = Modifier.testTag(carouselItemTag),
+ modifier = Modifier
+ .testTag(carouselItemTag),
background = {
Box(
modifier = Modifier
@@ -87,7 +87,8 @@
rule.waitForIdle()
// Check if overlay button in carousel item is focused
- rule.onNodeWithTag(sampleButtonTag).assertIsFocused()
+ rule.onNodeWithTag(sampleButtonTag, useUnmergedTree = true)
+ .assertIsFocused()
// Trigger back press
performKeyPress(NativeKeyEvent.KEYCODE_BACK)
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselTest.kt
index 7582b2c..b8eb11f 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CarouselTest.kt
@@ -275,7 +275,6 @@
rule.onNodeWithText("Text 2").assertIsDisplayed()
}
- @OptIn(ExperimentalAnimationApi::class)
@Test
fun carousel_pagerIndicatorDisplayed() {
rule.setContent {
@@ -287,7 +286,6 @@
rule.onNodeWithTag("indicator").assertIsDisplayed()
}
- @OptIn(ExperimentalAnimationApi::class)
@Test
fun carousel_withAnimatedContent_successfulTransition() {
rule.setContent {
@@ -308,7 +306,6 @@
rule.onNodeWithText("PLAY").assertIsDisplayed()
}
- @OptIn(ExperimentalAnimationApi::class)
@Test
fun carousel_withAnimatedContent_successfulFocusIn() {
rule.setContent {
@@ -326,8 +323,9 @@
rule.mainClock.advanceTimeBy(animationTime, false)
rule.mainClock.advanceTimeByFrame()
- rule.onNodeWithText("Play 0").assertIsDisplayed()
- rule.onNodeWithText("Play 0").assertIsFocused()
+ rule.onNodeWithText("Play 0", useUnmergedTree = true)
+ .assertIsDisplayed()
+ .assertIsFocused()
}
@Test
@@ -353,7 +351,7 @@
rule.waitForIdle()
// Check if the overlay button is focused
- rule.onNodeWithText("Button-1").assertIsFocused()
+ rule.onNodeWithText("Button-1", useUnmergedTree = true).assertIsFocused()
// Trigger back press event to exit focus
performKeyPress(NativeKeyEvent.KEYCODE_BACK)
@@ -361,11 +359,10 @@
rule.waitForIdle()
// Check if carousel loses focus and parent container gains focus
- rule.onNodeWithText("Button-1").assertIsNotFocused()
+ rule.onNodeWithText("Button-1", useUnmergedTree = true).assertIsNotFocused()
rule.onNodeWithTag("box-container").assertIsFocused()
}
- @OptIn(ExperimentalAnimationApi::class)
@Test
fun carousel_withCarouselItem_parentContainerGainsFocus_onBackPress() {
rule.setContent {
@@ -391,7 +388,7 @@
rule.waitForIdle()
// Check if the overlay button is focused
- rule.onNodeWithText("Play 0").assertIsFocused()
+ rule.onNodeWithText("Play 0", useUnmergedTree = true).assertIsFocused()
// Trigger back press event to exit focus
performKeyPress(NativeKeyEvent.KEYCODE_BACK)
@@ -399,11 +396,10 @@
rule.waitForIdle()
// Check if carousel loses focus and parent container gains focus
- rule.onNodeWithText("Play 0").assertIsNotFocused()
+ rule.onNodeWithText("Play 0", useUnmergedTree = true).assertIsNotFocused()
rule.onNodeWithTag("box-container").assertIsFocused()
}
- @OptIn(ExperimentalAnimationApi::class)
@Test
fun carousel_scrollToRegainFocus_checkBringIntoView() {
val focusRequester = FocusRequester()
@@ -491,7 +487,6 @@
assertThat(checkNodeCompletelyVisible(rule, "featured-carousel")).isTrue()
}
- @OptIn(ExperimentalAnimationApi::class)
@Test
fun carousel_zeroItemCount_shouldNotCrash() {
val testTag = "emptyCarousel"
@@ -502,7 +497,6 @@
rule.onNodeWithTag(testTag).assertExists()
}
- @OptIn(ExperimentalAnimationApi::class)
@Test
fun carousel_oneItemCount_shouldNotCrash() {
val testTag = "emptyCarousel"
@@ -571,7 +565,6 @@
rule.onNodeWithText("Button-1").assertIsFocused()
}
- @OptIn(ExperimentalAnimationApi::class)
@Test
fun carousel_manualScrolling_fastMultipleKeyPresses() {
val carouselState = CarouselState()
@@ -615,13 +608,13 @@
rule.mainClock.advanceTimeBy(animationTime)
val finalItem = itemProgression.sum()
- rule.onNodeWithText("Play $finalItem").assertIsFocused()
+ rule.onNodeWithText("Play $finalItem", useUnmergedTree = true).assertIsFocused()
performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT, 3)
rule.mainClock.advanceTimeBy((animationTime) * 3)
- rule.onNodeWithText("Play ${finalItem + 3}").assertIsFocused()
+ rule.onNodeWithText("Play ${finalItem + 3}", useUnmergedTree = true).assertIsFocused()
}
@Test
@@ -798,7 +791,7 @@
}
}
-@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalAnimationApi::class)
+@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
private fun SampleCarousel(
carouselState: CarouselState = remember { CarouselState() },
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Carousel.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Carousel.kt
index 50602d5..bf97f92 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/Carousel.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Carousel.kt
@@ -62,6 +62,9 @@
import androidx.compose.ui.input.key.type
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.semantics.CollectionInfo
+import androidx.compose.ui.semantics.collectionInfo
+import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
@@ -127,6 +130,9 @@
onAutoScrollChange = { isAutoScrollActive = it })
Box(modifier = modifier
+ .semantics {
+ collectionInfo = CollectionInfo(rowCount = 1, columnCount = itemCount)
+ }
.bringIntoViewIfChildrenAreFocused()
.focusRequester(carouselOuterBoxFocusRequester)
.onFocusChanged {
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/CarouselItem.kt b/tv/tv-material/src/main/java/androidx/tv/material3/CarouselItem.kt
index 2996e8d..bb10fd9 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/CarouselItem.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/CarouselItem.kt
@@ -16,12 +16,15 @@
package androidx.tv.material3
+import android.content.Context
+import android.view.accessibility.AccessibilityManager
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ContentTransform
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
@@ -37,8 +40,13 @@
import androidx.compose.ui.focus.FocusState
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.input.key.onKeyEvent
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.semantics.CollectionItemInfo
+import androidx.compose.ui.semantics.collectionItemInfo
+import androidx.compose.ui.semantics.isContainer
+import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.LayoutDirection
import androidx.tv.material3.KeyEventPropagation.ContinuePropagation
@@ -67,6 +75,10 @@
CarouselItemDefaults.contentTransformStartToEnd,
content: @Composable () -> Unit,
) {
+ val context = LocalContext.current
+ val accessibilityManager = remember {
+ context.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
+ }
var containerBoxFocusState: FocusState? by remember { mutableStateOf(null) }
val focusManager = LocalFocusManager.current
var exitFocus by remember { mutableStateOf(false) }
@@ -81,6 +93,16 @@
// This box holds the focus until the overlay animation completes
Box(
modifier = modifier
+ .semantics(mergeDescendants = true) {
+ isContainer = true
+ collectionItemInfo =
+ CollectionItemInfo(
+ rowIndex = 0,
+ rowSpan = 1,
+ columnIndex = itemIndex,
+ columnSpan = 1
+ )
+ }
.onKeyEvent {
exitFocus = it.isBackPress() && it.isTypeKeyDown()
ContinuePropagation
@@ -92,7 +114,14 @@
exitFocus = false
}
}
- .focusable()
+ .then(
+ if (accessibilityManager.isEnabled)
+ Modifier.clickable {
+ focusManager.moveFocus(FocusDirection.Enter)
+ }
+ else
+ Modifier.focusable()
+ )
) {
background()
@@ -102,7 +131,10 @@
exit = contentTransform.initialContentExit,
) {
LaunchedEffect(transition.isRunning, containerBoxFocusState?.isFocused) {
- if (!transition.isRunning && containerBoxFocusState?.isFocused == true) {
+ if (!transition.isRunning &&
+ containerBoxFocusState?.isFocused == true &&
+ !accessibilityManager.isEnabled
+ ) {
focusManager.moveFocus(FocusDirection.Enter)
}
}