How to prevent accessibility focus from moving to controls behind ExposedDropdownMenuBox - android-jetpack-compose

I am having an issue where the accessibility focus is going to controls behind the PopUp Window when using a ExposedDropdownMenuBox
If there is a single ExposedDropdownMenuBox everything works as expected, but when I add a second ExposedDropdownMenuBox or another control the focus goes to the second ExposedDropdownMenuBox before going to the PopUp Window.
GIF of single dropdown behavior
https://giphy.com/gifs/gapy0XK1CGmbyltJxU
GIF of two dropdowns on the same screen
https://giphy.com/gifs/WkL5TcMWlumfcGHPmD
Source
#Composable
fun Screen() {
Column (
modifier = Modifier
.wrapContentSize(Alignment.TopCenter)
.padding(top = 48.dp)
) {
Text(
text = stringResource(id = R.string.greeting),
fontSize = 30.sp,
modifier = Modifier.padding(bottom = 24.dp)
)
LocaleDropdownMenu()
Spacer(modifier = Modifier.height(8.dp))
// LocaleDropdownMenu()
}
}
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun LocaleDropdownMenu() {
val localeOptions = mapOf(
R.string.en to "en",
R.string.fr to "fr",
R.string.hi to "hi",
R.string.ja to "ja"
).mapKeys { stringResource(it.key) }
// boilerplate: https://developer.android.com/reference/kotlin/androidx/compose/material/package-summary#ExposedDropdownMenuBox(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1)
var expanded by remember { mutableStateOf(false) }
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = {
expanded = !expanded
}
) {
TextField(
readOnly = true,
value = stringResource(R.string.language),
onValueChange = { },
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(
expanded = expanded
)
}
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = {
expanded = false
}
) {
localeOptions.keys.forEach { selectionLocale ->
DropdownMenuItem(
onClick = {
expanded = false
// set app locale given the user's selected locale
AppCompatDelegate.setApplicationLocales(
LocaleListCompat.forLanguageTags(
localeOptions[selectionLocale]
)
)
},
content = { Text(selectionLocale) }
)
}
}
}
}
A repository that reproduces this issue is here:
https://github.com/dazza5000/ExposedDropdownMenuBox-accessibility-issue

Related

jetpack compose lazycolumn items not shows deleted only after scroll

Could you pelase help me?
I want to use swipe to delete and tried a lot of things, but nothing works good. The best way is:
items(listactivity.size) { index ->
val activityItem = listactivity.getOrNull(index)
if (activityItem != null) {
key(activityItem) {
val dismissState = rememberDismissState()
LaunchedEffect(dismissState.currentValue) {
if (dismissState.currentValue == DismissValue.DismissedToStart) {
dashboardViewModel.activityForDelete = activityItem
println("ACT ITEM " + activityItem.activityType?.activityTypeName)
visibleDeleteDialog.value = true
dismissState.snapTo(DismissValue.Default)
}
}
if (visibleDeleteDialog.value) {
BaseAlertDialog(
onExit = {
visibleDeleteDialog.value = false
},
onSuccess = {
removeActivity()
visibleDeleteDialog.value = false
},
disclaimerFirst = R.string.confirm_delete_activity,
disclaimerSecond = R.string.confirm_delete_activity_text,
successName = R.string.delete_session
)
}
SwipeToDismiss(
state = dismissState,
directions = setOf(DismissDirection.EndToStart),
background = {
val direction =
dismissState.dismissDirection ?: return#SwipeToDismiss
val alignment = when (direction) {
DismissDirection.StartToEnd -> Alignment.CenterStart
DismissDirection.EndToStart -> CenterEnd
}
Box(
modifier = Modifier
.fillMaxSize()
.background(Black)
.padding(horizontal = Spacing_12),
contentAlignment = alignment
) {
Text(
text = stringResource(id = R.string.delete),
color = Color.Red,
fontSize = Text_16,
fontWeight = FontWeight.Bold
)
}
},
dismissContent = {
GameCard(activityItem)
}
)
}
}
}
However th lisitem do not update items after delete immidiately (only after scroll i can see what item was deleted).
What am i doing wrong?
I tried SwipeToDismiss and other internet things.
I know google has a bug in this function items() some times,
but please try to replace your function with itemsIndexed and add another parameter
itemsIndexed(listactivity.size) { index , item ->
something like this
// import this at the top
import androidx.compose.foundation.lazy.itemsIndexed
//code
//*
//*
//*
itemsIndexed(listactivity.size) { index , item ->
val activityItem = listactivity.getOrNull(index)
if (activityItem != null) {
key(activityItem) {
val dismissState = rememberDismissState()
LaunchedEffect(dismissState.currentValue) {
if (dismissState.currentValue == DismissValue.DismissedToStart) {
dashboardViewModel.activityForDelete = activityItem
println("ACT ITEM " + activityItem.activityType?.activityTypeName)
visibleDeleteDialog.value = true
dismissState.snapTo(DismissValue.Default)
}
}
if (visibleDeleteDialog.value) {
BaseAlertDialog(
onExit = {
visibleDeleteDialog.value = false
},
onSuccess = {
removeActivity()
visibleDeleteDialog.value = false
},
disclaimerFirst = R.string.confirm_delete_activity,
disclaimerSecond = R.string.confirm_delete_activity_text,
successName = R.string.delete_session
)
}
SwipeToDismiss(
state = dismissState,
directions = setOf(DismissDirection.EndToStart),
background = {
val direction =
dismissState.dismissDirection ?: return#SwipeToDismiss
val alignment = when (direction) {
DismissDirection.StartToEnd -> Alignment.CenterStart
DismissDirection.EndToStart -> CenterEnd
}
Box(
modifier = Modifier
.fillMaxSize()
.background(Black)
.padding(horizontal = Spacing_12),
contentAlignment = alignment
) {
Text(
text = stringResource(id = R.string.delete),
color = Color.Red,
fontSize = Text_16,
fontWeight = FontWeight.Bold
)
}
},
dismissContent = {
GameCard(activityItem)
}
)
}
}
}
I don't have the time to go over all of your code to make more changes that maybe will make it a better fit for your code and needs. but I am sure you are able to do so!
you can see my code when I had a similar problem maybe it will help to solve your problem.
itemsIndexed(celebsSearchList) { index, item ->
if (celebsSearchList.isEmpty()) {
println("no celebs")
} else {
val celebie = item
// celebsSearchList.forEach { celebie ->
Card(
modifier = Modifier
.fillMaxWidth()
.padding(start = 20.dp, end = 20.dp, top = 10.dp, bottom = 10.dp),
elevation = 8.dp
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.medium),
horizontalArrangement = Arrangement.SpaceAround,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = celebie.FirstName + " " + celebie.LastName,
style = MaterialTheme.typography.h6
)
Button(
onClick = {
CelebForCelebProfile = celebie
navController.navigate(Screen.CelebProfileScreen.route)
}, colors = ButtonDefaults.buttonColors(
backgroundColor = Color.White, contentColor = Color.Gray
)
) {
Text(text = "View Profile")
}
}
}
}
}```
if you like me please upvote me :) help me gain more points on stack over flow and comment if you have any questions.

Updating LazyColumn In Card Button Click

I am developing a Question/Answer Quiz App in JetPack Compose. I have the Card as below
Card(modifier = Modifier
.wrapContentHeight(Alignment.CenterVertically)
.wrapContentWidth(Alignment.CenterHorizontally)
.padding(10.dp)
.width(300.dp)
.height(600.dp)
.clip(RoundedCornerShape(8.dp)),
elevation = 10.dp,
backgroundColor = Color.White
)
{
Column(
modifier = Modifier
.wrapContentHeight(Alignment.CenterVertically)
.wrapContentWidth(Alignment.CenterHorizontally)
.padding(8.dp),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top
){
Text(
text = "Question : " + query.question_id,
style = Typography.h1
)
Spacer(modifier = Modifier.height(2.dp))
Text(
text = query.question,
style = Typography.subtitle1
)
Spacer(modifier = Modifier.height(5.dp))
Text(
text = "Options",
style = Typography.h1
)
Spacer(modifier = Modifier.height(2.dp))
OptionsDetailsList(lstOptions = lstOptions)
Spacer(modifier = Modifier.height(10.dp))
Button(onClick = {
}
) {
Text(text = "View Answer")
}
Spacer(modifier = Modifier.height(10.dp))
}
}
}
The Answer List is a LazyColumn as below
#Composable
fun OptionsDetailsList(lstOptions: List<CertAnswers>){
Log.d("ListOptions Count" , lstOptions.size.toString())
LazyColumn(){
item {
Spacer(modifier = Modifier.requiredHeight(1.dp))
}
items(lstOptions){
item ->
Text(text = item.answer,
modifier=Modifier.padding(3.dp),
style = Typography.subtitle1,
)
Spacer(modifier = Modifier.requiredHeight(1.dp))
}
}
}
My Answers DataModel is as Below
data class CertAnswers(
#PrimaryKey (autoGenerate = true)
val id : Int,
var question_id : Int,
val ans_title: String,
val answer : String,
var isSolution: Boolean
)
With isSolution I can get whether an option is right answer.
But I want to show only on the button click and update LazyColumn to show the relevant option in different color.
I tried to call the OptionsDetailsList method again on button click but its not working.
How can i update the LazyColumn on Button Click within CardView.
You need to store a state value in your CardView indicating wether the button was tapped. rememberSaveable will make sure it's saved during recompositions and scrolling. I pass query.question_id as a key: in case it'll change most probably the value should be reinitialized. Check out more about state in compose in documentation
var answerRevealed by rememberSaveable(query.question_id) { mutableStateOf(false) }
You can change the background of the lazy column elements depending on this state. p.s. I suggest you use Modifier as the last argument, so you don't need a comma at the end and you can add/remove/reorder modifiers easily:
#Composable
fun OptionsDetailsList(answerRevealed: Boolean, lstOptions: List<CertAnswers>) {
Log.d("ListOptions Count", lstOptions.size.toString())
LazyColumn() {
item {
Spacer(modifier = Modifier.requiredHeight(1.dp))
}
items(lstOptions) { item ->
Text(
text = item.answer,
style = Typography().subtitle1,
modifier = Modifier
.padding(3.dp)
.background(
if (answerRevealed) {
if (item.isSolution) {
Color.Green
} else {
Color.Red
}
} else {
Color.Transparent
}
)
)
Spacer(modifier = Modifier.requiredHeight(1.dp))
}
}
}
In your button just set this state to true. You can also hide the button once it's tapped, or change the text.
// hide after onClick
if (!answerRevealed) {
Button(onClick = {
answerRevealed = true
}
) {
Text(text = "View Answer")
}
}
// or change text
Button(onClick = {
answerRevealed = true
}
) {
Text(text = if (answerRevealed) "Hide Answer" else "View Answer")
}

LazyColumn does not scroll if using TextFields as child

#Composable
fun init() {
LazyColumn(Modifier.fillMaxSize()) {
for (i in 0..10) {
item { Box(Modifier.padding(15.dp)) { TextField("Hallo$i", modifier = Modifier.fillMaxWidth(), onValueChange = {}) } }
}
}
}
If i have something simple as this list with textfields
then the textfields will not let me scroll down the column.
Only works if i scroll down next to the textfields.
Tried also with readonly/disabled textfield.
is there a way to overcome this behaviour?
maybe a way to disable focus on textfield if scrolled?
I am using jetbrains-compose for desktop version (0.5.0-build245)
but can also be the same as in the jetpack-compose for android (did not try)
for the moment because i don't find any other solution i will use this workaround
using a invisible box above the text field and change the state accordingly
#Composable
fun init() {
LazyColumn(Modifier.fillMaxSize()) {
for (i in 0..10) {
item {
val isfocused = remember { mutableStateOf(false) }
val focusRequester = FocusRequester()
Box(Modifier.padding(15.dp)) {
TextField("Hallo$i", modifier = Modifier.fillMaxWidth().focusRequester(focusRequester).onFocusChanged {
isfocused.value = it.isFocused
}, onValueChange = {})
if (!isfocused.value)
Box(
modifier = Modifier
.matchParentSize()
.alpha(0f)
.clickable(onClick = {
isfocused.value = true
focusRequester.requestFocus()
}),
)
}
}
}
}
}

Android Jetpack Compose Row's height (IntrinsicSize.Min) is not stretched when children column generate more composables

The following is my code snippet. I pass in editClick that adds a data class object into chargingViewModel.contractSelfPay, which is observed as itemList state. When I click the icon, I can tell itemList state receives update by having more edit icons that are spaced evenly. However, BasicGrid Row's height is not stretched with Intrinsic.Min.
If I remove IntrinsicSize.Min, even though row's height is stretched, dividers no longer can fillMaxHeight as well as icon columns. without Intrinsic.Min
#Composable
fun ContractSelfPay(chargingViewModel: ChargingViewModel, editClick: () -> Unit = {}) {
val itemList by chargingViewModel.contractSelfPay.observeAsState()
val composeList: List<#Composable () -> Unit> = itemList?.map {
#Composable {
Row {
TempFunc { StyledText(text = it.itemTitle) }
TempFunc { StyledText(text = it.originalPrice.toString()) }
TempFunc { StyledText(text = it.selfPay.toString(), color = self_pay_blue) }
TempFunc { StyledText(text = it.count.toString()) }
TempFunc { StyledText(text = (it.selfPay * it.count).toString()) }
}
}
} ?: listOf()
val total = itemList?.map { (it.selfPay.toInt() * it.count.toInt()) }?.sum() ?: 0
BasicGrid("全自費", composeList, total = total.toString(), editClick = editClick)
}
#Composable
fun BasicGrid(
gridTitle: String,
itemList: List<#Composable () -> Unit>,
total: String = "0",
editClick: () -> Unit = {}
) {
Row(modifier = Modifier.height(IntrinsicSize.Min), verticalAlignment = Alignment.CenterVertically) {
StyledTextBold(text = gridTitle, modifier = Modifier.weight(15f).wrapContentWidth())
VerticalDivider()
Column(
modifier = Modifier.weight(60f)
) {
itemList.forEachIndexed { index, compose ->
compose()
if (index != itemList.size - 1)
HorizontalDivider()
}
if (itemList.isEmpty())
StyledText(text = "尚未有任何紀錄", modifier = Modifier.weight(1f).wrapContentSize())
}
VerticalDivider()
StyledTextBold(text = total, modifier = Modifier.weight(15f).wrapContentWidth())
VerticalDivider()
Column(
modifier = Modifier
.weight(10f)
.fillMaxHeight(),
verticalArrangement = Arrangement.SpaceEvenly
) {
itemList.forEachIndexed { index, detail ->
Image(
painter = painterResource(R.drawable.icon_mode_edit),
contentDescription = "",
modifier = Modifier
.align(Alignment.CenterHorizontally)
.clickable { editClick() },
)
if (itemList.isNotEmpty() && index != itemList.size - 1)
HorizontalDivider()
}
}
}
}
I have created issue here https://issuetracker.google.com/issues/217910352. Hopefully it gets solved.
One of the work-arounds I could think of is keeping track of height and removing IntrinsicSize.Min.
As in:
// _key_ is something that causes change of the height of the row
var height by remember(_key_) { mutableStateOf(0) }
Row(Modifier.onSizeChanged { height = it.height }) {
VerticalDivider(Modifier.height(height))
}
In your case I suppose key would be size of itemList.
Thank you Majkeee. It's been a while. The way I fixed it at the time was with custom layout modifier. Not sure if it still works today though.
fun Modifier.expandHeight() = this.then(layout { measurable, constraints ->
val placeable =
measurable.measure(constraints.copy(maxHeight = Constraints.Infinity))
layout(placeable.width, placeable.height) {
placeable.placeRelative(0, 0)
}
})
and to use it you can do
Column(modifier = Modifier.expandHeight())

How to add swipe behavior to the screens?

We have bottom navigation in our app and we want to add swipe behavior into our screens so that if a user swipe to right/left then s/he should be navigated into next screen.
I know that Accompanist has HorizontalPager with Tabs. But I wonder if we can achieve that behavior with bottom navigation.
As you can see in the Material Design Guidelines:
Using swipe gestures on the content area does not navigate between views.
Also:
Avoid using lateral motion to transition between views.
But, if you really want to do this, you can do the this:
fun BottomNavSwipeScreen() {
// This scope is necessary to change the tab using animation
val scope = rememberCoroutineScope()
// I'm using a list of images here
val images = listOf(R.drawable.img1, ...)
// This page state will be used by BottomAppbar and HorizontalPager
val pageState = rememberPagerState(pageCount = images.size)
val scaffoldState = rememberScaffoldState()
Scaffold(
scaffoldState = scaffoldState,
bottomBar = {
BottomAppBar(
backgroundColor = MaterialTheme.colors.primary,
content = {
for (page in images.indices) {
BottomNavigationItem(
icon = {
Icon(Icons.Filled.Home, "Page $page")
},
// here's the trick. the selected tab is based
// on HorizontalPager state.
selected = page == pageState.currentPage,
onClick = {
// When a tab is selected,
// the page is updated
scope.launch {
pageState.animateScrollToPage(page)
}
},
selectedContentColor = Color.Magenta,
unselectedContentColor = Color.LightGray,
label = { Text(text = "Page $page") }
)
}
}
)
},
) {
HorizontalPager(
state = pageState,
offscreenLimit = 2
) { page ->
Image(
painterResource(id = images[page]),
null,
modifier = Modifier
.fillMaxSize(),
contentScale = ContentScale.Crop
)
}
}
}
Here is the result:
you can achieve this by using the animation library from compose:
https://developer.android.com/jetpack/compose/animation
And using the slideIntoContainer animation you can simulate the swipe effect:
composable("route1",
enterTransition = {
slideIntoContainer(
towards = AnimatedContentScope.SlideDirection.Right,
animationSpec = tween(
durationMillis = 250,
easing = LinearEasing // interpolator
)
)
},
exitTransition = {
slideOutOfContainer(
towards = AnimatedContentScope.SlideDirection.Left,
animationSpec = tween(
durationMillis = 250,
easing = LinearEasing
)
)
}) {
Screen1()
}
composable("route2",
enterTransition = {
slideIntoContainer(
towards = AnimatedContentScope.SlideDirection.Left,
animationSpec = tween(
durationMillis = 250,
easing = LinearEasing // interpolator
)
)
},
exitTransition = {
slideOutOfContainer(
towards = AnimatedContentScope.SlideDirection.Right,
animationSpec = tween(
durationMillis = 250,
easing = LinearEasing
)
)
}) {
Screen2()
}

Resources