How to make ClickableText accessible to screen readers - android-jetpack-compose

This code creates a ClickableText element in Jetpack Compose Composable:
ClickableText(
text = forgotPasswordAnnotatedString,
onClick = {
context.startActivity(intent)
},
modifier = Modifier
.padding(top = mediumPadding)
)
The annotated string is defined here to make the text look like a link:
val forgotPasswordAnnotatedString = buildAnnotatedString {
append(stringResource(R.string.forgot_password))
addStyle(
style = SpanStyle(
textDecoration = TextDecoration.Underline,
color = Color.White,
fontSize = 16.sp,
fontWeight = FontWeight.Medium
),
start = 0,
end = 21,
)
}
When I encounter this text using the TalkBalk screen reader in Android, the screenreader does not make it clear that this is clickable text that will do something which tapped on. The reader just reads the text.
Is there a way to make it clear to the screen reader that this text is interactive? Otherwise should I just use a button and style it to look like a link?

It looks like your intention is for the whole text to be clickable? In which you best option is probably a TextButton as suggested by
Gabriele Mariotti.
But if you wan't only part of the link to be clickable, or to have multiple clickable sections, the best I've been able to land on is to draw an invisible box overtop of the Text. It means that I can control the touch target of the clickable area to be at least 48.dp and can use the semantics{} modifier to control how a screen reader interprets it.
Would welcome any suggestions.
// remember variables to hold the start and end position of the clickable text
val startX = remember { mutableStateOf(0f) }
val endX = remember { mutableStateOf(0f) }
// convert to Dp and work out width of button
val buttonPaddingX = with(LocalDensity.current) { startX.value.toDp() }
val buttonWidth = with(LocalDensity.current) { (endX.value - startX.value).toDp() }
Text(
text = forgotPasswordAnnotatedString,
onTextLayout = {
startX.value = it.getBoundingBox(0).left // where 0 is the start index of the range you want to be clickable
endX.value = it.getBoundingBox(21 - 1).right // where 21 is the end index of the range you want to be clickable
}
)
Note that buttonPaddingX is relative to the Text position not the Window, so you may have to surround both in a Box{} or use ConstraintLayout.
Then to draw the invisible box
Box(modifier = Modifier
.sizeIn(minWidth = 48.dp, minHeight = 48.dp) // minimum touch target size
.padding(start = buttonPaddingX)
.width(buttonWidth)
// .background(Color.Magenta.copy(alpha = 0.5f)) // uncomment this to debug where the box is drawn
.clickable(onClick = { context.startActivity(intent) })
.semantics {
// tell TalkBack whatever you need to here
role = Role.Button
contentDescription = "Insert button description here"
}
)
In my code I'm using pushStringAnnotation(TAG, annotation) rather than reference string indexes directly. That way I can get the start and end index of the clickable area with annotatedString.getStringAnnotations(TAG,0,annotatedString.length).first(). Useful if there a multiple links within the text.
It's disappointing that ClickableText doesn't have accessibility in mind from the get go, hopefully we'll be able to use it again in a future update.

Adding .semantics.contentDescription to the Modifier changes what is read by the screen reader. I had to word contentDescription to make it clear that this was a link to reset the your password.
The screen reader still doesn't recognize the element a clickable but hopefully the description will be useful to convey to the user that this element is interactive.
ClickableText(
text = forgotPasswordAnnotatedString,
onClick = {
context.startActivity(intent)
},
modifier = Modifier
.padding(top = mediumPadding)
// new code here:
.semantics {
contentDescription = "Forgot your password? link"
}
)

Related

How to set the cursor to any part of the text using TextFieldValue (w/ FocusRequester) on pressing/clicking the TextField

I tried to look around but I can't find a way to
force a focus,
set the cursor to the end of text
and still be able to set the cursor to any part of the text when
pressing/clicking.
With FocusRequester the cursor is set to the start of text, but with TextFieldValue(<text>, <range>) I'm able to place the cursor at the end, the problem is that with this approach the cursor is always forced to any specified selection = TextRange(<index>)(in my case its the end using the current length of the changing value onValueChange), I have no idea how to set the cursor in any part (selection) when I press/click the TextField.
My Implementation:
var textFieldValue by remember { mutableStateOf(TextFieldValue("Some Text")) }
Row (
modifier = Modifier
.height(150.dp)
.fillMaxWidth()
.clickable {
focusRequester.requestFocus()
textFieldValue = textFieldValue.copy(selection = TextRange("Some Text".length))
}
) {
BasicTextField(
modifier = Modifier.focusRequester(focusRequester),
value = textFieldValue,
onValueChange = {
textFieldValue =
textFieldValue.copy(text = it.text, selection = TextRange(it.text.length))
},
)
}
And what I'm trying to achieve (Large text area with text set and starts from top-left), its entire part should be clickable and triggers focus for the text field, the reason why I wrapped it in a Row with a clickable modifier.
I wasn't able to achieve this with a single text field with specified height, as TextAlign doesn't have a Top-Start alignment

How do I fix ConstraintLayout that include Balloon(gives strange results) in jetpack compose?

I'm trying to make a help pop ups in my first app. the problem that came up after making the pop up work is that the icon which I'm using becomes a button taking up whole screen height.
I'm using the only code I found for balloon popups in jetpack compose.
the layout is fine until I add the BalloonAnchor.
this is the code:
#Composable
fun GiveHelp(helpText: String) {
Surface{
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
ConstraintLayout {
val (icon, text) = createRefs()
Icon(
modifier = Modifier
.constrainAs(icon) {
top.linkTo(parent.top)
start.linkTo(parent.start)
},
painter = painterResource(id = R.drawable.ic_help),
contentDescription = "help Icon"
)
Text(
modifier = Modifier
.constrainAs(text) {
top.linkTo(icon.top)
start.linkTo(icon.end)
bottom.linkTo(icon.bottom)
}
.padding(horizontal = 10.dp),
text = "Is your task:"
)
BalloonAnchor(
reference = icon,
modifier = Modifier
.aspectRatio(0.1f),
balloon = BalloonUtils.getTitleBalloon(
context = context,
title = helpText,
lifecycle = lifecycleOwner
),
onAnchorClick = { balloon, anchor -> balloon.showAlignTop(anchor) }
)
}
}
}
The problem here is the aspectRatio you are using in the Modifier of BalloonAnchor. Try something like Modifier.aspectRatio(0.99f). Using this, your Icon will not take the entire screen height. Or, your can use something like below code to get a desirable look.
BalloonAnchor(
reference = icon,
modifier = Modifier
.height(40.dp),
balloon = BalloonUtils.getTitleBalloon(
context = context,
title = helpText,
lifecycle = lifecycleOwner
),
onAnchorClick = { balloon, anchor -> balloon.showAlignTop(anchor) }
)

Change highlight on clickable Text possible?

I have a #Composable Text("Dropbox") whose Modifier also implements a
clickable {
//..
}
But when I tapping on it
, it gets a grey highlight!?
I do not want this grey hightlight. Can I get rid of this?
Here indication is responsible for showing the highlight.
So making indication = null should do the job
.clickable(
onClick = {
//..
},
indication = null,
interactionSource = remember { MutableInteractionSource() }
)

is there a way to change the background color of a specific word in outlined text field jetpack compose?

I am looking for a way to change the background color of certain words in outlined text field while typing, but I couldn't find a way to achieve that yet. Does anyone know a way to do this? (Preferably in compose but other solutions are also OK.)
You can use the VisualTransformation property in the TextField to change the text while typing. Use an AnnotatedString to apply different SpanStyle to your text.
Something like:
OutlinedTextField(
value = text,
onValueChange = { text = it },
visualTransformation = ColorsTransformation()
)
class ColorsTransformation() : VisualTransformation {
override fun filter(text: AnnotatedString): TransformedText {
return TransformedText(
buildAnnotatedStringWithColors(text.toString()),
OffsetMapping.Identity)
}
}
In the buildAnnotatedStringWithColors you can move your logic to change the background color of certain words. It is just an example to change the color for each word.
fun buildAnnotatedStringWithColors(text:String): AnnotatedString{
//it is just an example
val words: List<String> = text.split("\\s+".toRegex())// splits by whitespace
val builder = AnnotatedString.Builder()
val colors = listOf(Color.Red,Color.Black,Color.Yellow,Color.Blue)
var count = 0
for (word in words) {
//Use you favorite SpanStyle
builder.withStyle(style = SpanStyle(
background = colors[count % 4],
color = White
)) {
append("$word ")
}
count ++
}
return builder.toAnnotatedString()
}
The simplest solution is using annotated string:
Text(buildAnnotatedString {
append("Hello ")
withStyle(
style = SpanStyle(
fontWeight = FontWeight.Bold,
color = Color.White,
background = Color.Blue,
)
) {
append("World")
}
}, modifier = Modifier.padding(10.dp))
Result
Also take a look at this solution, which will allow you to use a more complex drawing like this:

How to make the width of a Text equal to the width of another implicitly measured Text?

There is a reader app with two Texts, one for the total number of pages and one for the current page. I want the width of the current page Text to be equal to the width of the total page Text, but the width of the total page Text is measured implicitly.
Currently, I can only specify the width like this:
Row {
Text(modifier = Modifier.width(32.dp), text = "1000") // total page
Slider(...)
Text(modifier = Modifier.width(32.dp), text = "1") // current page
}
To get render text layout, you can use onTextLayout. It has all information you may need, including size.
Then you can pass this size to second Text modifier:
var totalPageTextWidth by remember { mutableStateOf<Int?>(null) }
val widthModifier = totalPageTextWidth?.let { width ->
with(LocalDensity.current) {
Modifier.width(width.toDp())
}
} ?: Modifier
Text(
text = "1000",
onTextLayout = { totalPageTextWidth = it.size.width }
)
Text(
text = "1",
modifier = widthModifier
)

Resources