From 2d8c5f479f590305b9fedc744e00fdc9c276bbb9 Mon Sep 17 00:00:00 2001 From: Andrei Salavei Date: Wed, 11 Dec 2024 13:18:42 +0100 Subject: [PATCH] Add Accessibility demos from Android Demo app (#1724) Add accessibility demos from Android App to common demos --- .../androidx/compose/mpp/demo/MainScreen.kt | 2 + .../AndroidAccessibilityDemos.kt | 35 ++ .../ComplexAccessibilityDemos.kt | 324 ++++++++++++++++++ .../mpp/demo/accessibility/ScrollingUIDemo.kt | 101 ++++++ 4 files changed, 462 insertions(+) create mode 100644 compose/mpp/demo/src/commonMain/kotlin/androidx/compose/mpp/demo/accessibility/AndroidAccessibilityDemos.kt create mode 100644 compose/mpp/demo/src/commonMain/kotlin/androidx/compose/mpp/demo/accessibility/ComplexAccessibilityDemos.kt create mode 100644 compose/mpp/demo/src/commonMain/kotlin/androidx/compose/mpp/demo/accessibility/ScrollingUIDemo.kt diff --git a/compose/mpp/demo/src/commonMain/kotlin/androidx/compose/mpp/demo/MainScreen.kt b/compose/mpp/demo/src/commonMain/kotlin/androidx/compose/mpp/demo/MainScreen.kt index ffe7cd7b89e68..f7d94d216a166 100644 --- a/compose/mpp/demo/src/commonMain/kotlin/androidx/compose/mpp/demo/MainScreen.kt +++ b/compose/mpp/demo/src/commonMain/kotlin/androidx/compose/mpp/demo/MainScreen.kt @@ -16,6 +16,7 @@ package androidx.compose.mpp.demo +import androidx.compose.mpp.demo.accessibility.AndroidAccessibilityDemos import androidx.compose.mpp.demo.bug.BugReproducers import androidx.compose.mpp.demo.components.Components import androidx.compose.mpp.demo.textfield.android.AndroidTextFieldSamples @@ -34,4 +35,5 @@ val MainScreen = Screen.Selection( Screen.Example("InteropOrder") { InteropOrder() }, AndroidTextFieldSamples, Screen.Example("Android TextBrushDemo") { TextBrushDemo() }, + AndroidAccessibilityDemos ) diff --git a/compose/mpp/demo/src/commonMain/kotlin/androidx/compose/mpp/demo/accessibility/AndroidAccessibilityDemos.kt b/compose/mpp/demo/src/commonMain/kotlin/androidx/compose/mpp/demo/accessibility/AndroidAccessibilityDemos.kt new file mode 100644 index 0000000000000..34b389dbb24a1 --- /dev/null +++ b/compose/mpp/demo/src/commonMain/kotlin/androidx/compose/mpp/demo/accessibility/AndroidAccessibilityDemos.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * 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 androidx.compose.mpp.demo.accessibility + +import androidx.compose.mpp.demo.Screen + +val AndroidAccessibilityDemos = Screen.Selection( + "Android Accessibility", + Screen.Example("Scaffold Top Bar") { ScaffoldSampleDemo() }, + Screen.Example("Scaffold with Scrolling") { ScaffoldSampleScrollDemo() }, + Screen.Example("Simple Top Bar with Scrolling") { ScrollingColumnDemo() }, + Screen.Example("Nested Containers—True") { NestedContainersTrueDemo() }, + Screen.Example("Nested Containers—False") { NestedContainersFalseDemo() }, + Screen.Example("Linear Progress Indicator") { LinearProgressIndicatorDemo() }, + Screen.Example("Dual LTR and RTL Scene") { SimpleRtlLayoutDemo() }, + Screen.Example("Scrolling Tooltip scene") { SampleScrollingTooltipScreen() }, + + // Additional demos: + Screen.Example("Nested Traversal Index Inheritance") { NestedTraversalIndexInheritanceDemo() }, + Screen.Example("Nested and Peer Traversal Index") { NestedAndPeerTraversalIndexDemo() } +) \ No newline at end of file diff --git a/compose/mpp/demo/src/commonMain/kotlin/androidx/compose/mpp/demo/accessibility/ComplexAccessibilityDemos.kt b/compose/mpp/demo/src/commonMain/kotlin/androidx/compose/mpp/demo/accessibility/ComplexAccessibilityDemos.kt new file mode 100644 index 0000000000000..16f94378cb4d2 --- /dev/null +++ b/compose/mpp/demo/src/commonMain/kotlin/androidx/compose/mpp/demo/accessibility/ComplexAccessibilityDemos.kt @@ -0,0 +1,324 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * 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 androidx.compose.mpp.demo.accessibility + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.BottomAppBar +import androidx.compose.material.DrawerValue +import androidx.compose.material.FabPosition +import androidx.compose.material.FloatingActionButton +import androidx.compose.material.Icon +import androidx.compose.material.LinearProgressIndicator +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.rememberDrawerState +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.isTraversalGroup +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.traversalIndex +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import kotlin.math.max + +@Composable +fun CardRow( + modifier: Modifier, + columnNumber: Int, + topSampleText: String, + bottomSampleText: String +) { + Row( + modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End + ) { + Column { + Text(topSampleText + columnNumber) + Text(bottomSampleText + columnNumber) + } + } +} + +@Composable +fun NestedContainersFalseDemo() { + val topSampleText = "Top text in column " + val bottomSampleText = "Bottom text in column " + Column(Modifier.testTag("Test Tag").semantics { isTraversalGroup = true }) { + Row { + Modifier.semantics { isTraversalGroup = true } + CardRow( + Modifier.semantics { isTraversalGroup = false }, + 1, + topSampleText, + bottomSampleText + ) + CardRow( + Modifier.semantics { isTraversalGroup = false }, + 2, + topSampleText, + bottomSampleText + ) + } + } +} + +@Composable +fun NestedContainersTrueDemo() { + val topSampleText = "Top text in column " + val bottomSampleText = "Bottom text in column " + Column(Modifier.testTag("Test Tag").semantics { isTraversalGroup = true }) { + Row { + Modifier.semantics { isTraversalGroup = true } + CardRow( + Modifier.semantics { isTraversalGroup = true }, + 1, + topSampleText, + bottomSampleText + ) + CardRow( + Modifier.semantics { isTraversalGroup = true }, + 2, + topSampleText, + bottomSampleText + ) + } + } +} + +@Composable +fun TopAppBar() { + val topAppBar = "Top App Bar" + TopAppBar(title = { Text(text = topAppBar) }) +} + +@Composable +fun ScrollColumn(padding: PaddingValues) { + var counter = 0 + val sampleText = "Sample text in column" + Column(Modifier.verticalScroll(rememberScrollState()).padding(padding).testTag("Test Tag")) { + repeat(100) { Text(sampleText + counter++) } + } +} + +@Composable +fun ScaffoldSampleDemo() { + val scaffoldState = rememberScaffoldState(rememberDrawerState(DrawerValue.Closed)) + Scaffold( + scaffoldState = scaffoldState, + topBar = { TopAppBar() }, + floatingActionButtonPosition = FabPosition.End, + floatingActionButton = { + FloatingActionButton(onClick = {}) { + Icon(imageVector = Icons.Default.Add, contentDescription = "fab icon") + } + }, + drawerContent = { Text(text = "Drawer Menu 1") }, + content = { padding -> Text("Content", modifier = Modifier.padding(padding)) }, + bottomBar = { + BottomAppBar(backgroundColor = MaterialTheme.colors.primary) { Text("Bottom App Bar") } + } + ) +} + +@Composable +fun ScaffoldSampleScrollDemo() { + val scaffoldState = rememberScaffoldState(rememberDrawerState(DrawerValue.Closed)) + Scaffold( + scaffoldState = scaffoldState, + topBar = { TopAppBar() }, + floatingActionButtonPosition = FabPosition.End, + floatingActionButton = { + FloatingActionButton(onClick = {}) { + Icon(imageVector = Icons.Default.Add, contentDescription = "fab icon") + } + }, + content = { padding -> ScrollColumn(padding) }, + bottomBar = { + BottomAppBar(backgroundColor = MaterialTheme.colors.primary) { Text("Bottom App Bar") } + } + ) +} + +@Composable +fun ScrollingColumnDemo() { + val sampleText = "Sample text in column" + var counter = 0 + + Column(Modifier.verticalScroll(rememberScrollState()).testTag("Test Tag")) { + TopAppBar() + repeat(100) { Text(sampleText + counter++) } + } +} + +@Composable +fun FloatingBox() { + Box( + modifier = + Modifier.semantics { + isTraversalGroup = true + traversalIndex = -1f + } + ) { + FloatingActionButton(onClick = {}) { + Icon(imageVector = Icons.Default.Add, contentDescription = "fab icon") + } + } +} + +@Composable +fun ContentColumn(padding: PaddingValues) { + var counter = 0 + val sampleText = "Sample text in column" + Column(Modifier.verticalScroll(rememberScrollState()).padding(padding).testTag("Test Tag")) { + // every other value has an explicitly set `traversalIndex` + Text(text = sampleText + counter++) + Text(text = sampleText + counter++, modifier = Modifier.semantics { traversalIndex = 1f }) + Text(text = sampleText + counter++) + Text(text = sampleText + counter++, modifier = Modifier.semantics { traversalIndex = 1f }) + Text(text = sampleText + counter++) + Text(text = sampleText + counter++, modifier = Modifier.semantics { traversalIndex = 1f }) + Text(text = sampleText + counter++) + } +} + +/** + * Example of how `traversalIndex` and traversal groups can be used to customize TalkBack ordering. + * The example below puts the FAB into a box (with `isTraversalGroup = true` and a custom traversal + * index) to have it appear first when TalkBack is turned on. The text in the column also has been + * modified. See go/traversal-index-changes for more detail + */ +@Composable +fun NestedTraversalIndexInheritanceDemo() { + val scaffoldState = rememberScaffoldState(rememberDrawerState(DrawerValue.Closed)) + Scaffold( + scaffoldState = scaffoldState, + topBar = { TopAppBar() }, + floatingActionButtonPosition = FabPosition.End, + floatingActionButton = { FloatingBox() }, + drawerContent = { Text(text = "Drawer Menu 1") }, + content = { padding -> ContentColumn(padding = padding) }, + bottomBar = { + BottomAppBar(backgroundColor = MaterialTheme.colors.primary) { Text("Bottom App Bar") } + } + ) +} + +@Composable +fun NestedAndPeerTraversalIndexDemo() { + Column( + Modifier + // Having a traversal index here as 8f shouldn't affect anything; this column + // has no other peers that its compared to + .semantics { + traversalIndex = 8f + isTraversalGroup = true + } + .padding(8.dp) + ) { + Row( + Modifier.semantics { + traversalIndex = 3f + isTraversalGroup = true + } + ) { + Column(modifier = Modifier.testTag("Text1")) { + Row { Text("text 3\n") } + Row { + Text(text = "text 5\n", modifier = Modifier.semantics { traversalIndex = 1f }) + } + Row { Text("text 4\n") } + } + } + Row { Text(text = "text 2\n", modifier = Modifier.semantics { traversalIndex = 2f }) } + Row { Text(text = "text 1\n", modifier = Modifier.semantics { traversalIndex = 1f }) } + Row { Text(text = "text 0\n") } + } +} + +@Composable +fun LinearProgressIndicatorDemo() { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text("LinearProgressIndicator with undefined progress") + Spacer(Modifier.height(30.dp)) + LinearProgressIndicator(modifier = Modifier.size(100.dp, 10.dp)) + } +} + +@Composable +fun SimpleRtlLayoutDemo() { + Column { + Row(Modifier.semantics { isTraversalGroup = true }) { + SimpleTestLayout(Modifier.requiredSize(100.dp)) { Text("Child 1") } + SimpleTestLayout(Modifier.requiredSize(100.dp)) { Text("Child 2") } + SimpleTestLayout(Modifier.requiredSize(100.dp)) { Text("Child 3") } + } + CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { + // Will display rtlChild3 rtlChild2 rtlChild1, but should be read + // from child1 => child2 => child3. + Row(Modifier.semantics { isTraversalGroup = true }) { + SimpleTestLayout(Modifier.requiredSize(100.dp)) { Text("RTL child 1") } + SimpleTestLayout(Modifier.requiredSize(100.dp)) { Text("RTL child 2") } + SimpleTestLayout(Modifier.requiredSize(100.dp)) { Text("RTL child 3") } + } + } + } +} + +@Composable +private fun SimpleTestLayout(modifier: Modifier = Modifier, content: @Composable () -> Unit) { + Layout(modifier = modifier, content = content) { measurables, constraints -> + if (measurables.isEmpty()) { + layout(constraints.minWidth, constraints.minHeight) {} + } else { + val placeables = measurables.map { it.measure(constraints) } + val (width, height) = + with(placeables) { + Pair( + max(maxByOrNull { it.width }?.width ?: 0, constraints.minWidth), + max(maxByOrNull { it.height }?.height ?: 0, constraints.minHeight) + ) + } + layout(width, height) { + for (placeable in placeables) { + placeable.placeRelative(0, 0) + } + } + } + } +} diff --git a/compose/mpp/demo/src/commonMain/kotlin/androidx/compose/mpp/demo/accessibility/ScrollingUIDemo.kt b/compose/mpp/demo/src/commonMain/kotlin/androidx/compose/mpp/demo/accessibility/ScrollingUIDemo.kt new file mode 100644 index 0000000000000..c8ba63ab56588 --- /dev/null +++ b/compose/mpp/demo/src/commonMain/kotlin/androidx/compose/mpp/demo/accessibility/ScrollingUIDemo.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * 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 androidx.compose.mpp.demo.accessibility + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Menu +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.PlainTooltip +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TooltipBox +import androidx.compose.material3.TooltipDefaults.rememberTooltipPositionProvider +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.rememberTooltipState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SampleScrollingTooltipScreen() { + Scaffold( + topBar = { + TopAppBar( + title = { Text("Sample Screen") }, + navigationIcon = { + TooltipBox( + positionProvider = rememberTooltipPositionProvider(), + tooltip = { PlainTooltip { Text(text = "Navigation icon") } }, + state = rememberTooltipState() + ) { + IconButton(onClick = {}) { + Icon( + imageVector = Icons.Default.Menu, + contentDescription = "Navigation icon" + ) + } + } + }, + actions = { + TooltipBox( + positionProvider = rememberTooltipPositionProvider(), + tooltip = { PlainTooltip { Text(text = "Search") } }, + state = rememberTooltipState() + ) { + IconButton(onClick = {}) { + Icon( + imageVector = Icons.Default.Search, + contentDescription = "Search icon" + ) + } + } + TooltipBox( + positionProvider = rememberTooltipPositionProvider(), + tooltip = { PlainTooltip { Text(text = "Settings") } }, + state = rememberTooltipState() + ) { + IconButton(onClick = {}) { + Icon( + imageVector = Icons.Default.Settings, + contentDescription = "Settings icon" + ) + } + } + } + ) + } + ) { paddingValues -> + LazyColumn( + modifier = Modifier.padding(paddingValues).fillMaxSize().padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(40) { index -> + Text(text = "Item ${index + 1}", style = MaterialTheme.typography.bodyLarge) + } + } + } +}