https://kotlinlang.org logo
Title
t

Travis

04/26/2023, 4:30 PM
Is there a way to request talkback focus on a particular element? I've tried
Modifier.semantics { focused = isMyElementFocused }
but this seems a little wrong since focus is an externally-tracked, transient thing, and in my case indeed it does not work. I'm showing a swipeable container and I want to focus on the handle element which is also clickable to dismiss the container. I want to grab focus when the container opens (since I'll be covering up the rest of the app and focus will be lost), but trying a focus requester, semantics modifier, or even regular focus doesn't seem to work for me. Focus just lands on an arbitrary element of the container or on the scrim covering the app. Any tips on what I might be doing wrong here?
j

Jonas

04/27/2023, 11:28 AM
As far as I know it’s currently not possible to change the a11y focus order with compose.
t

Travis

04/27/2023, 5:33 PM
I spent yesterday on this and it turns out you can request focus, but my issue was modifier ordering, and the solution is as weird as I thought it was. You must declare your semantics block where you set
focused = true
before you add the
focusable()
modifier. As a result it must be before your
clickable()
modifier, which adds a focusable modifier internally. Why does using
Modifier.semantics { focused = isFocused }
work? Because in AndroidComposeViewAccessibilityDelegateCompat's
sendSemanticsPropertyChangeEvents()
method we detect changes to semantic fields, convert those to accessibility actions, and send them to talkback tied to the virtualViewId of the compose element. The focused property requests focus on the virtual view (the composable in question), which then sends the ACTION_ACCESSIBILITY_FOCUS event to the delegate, which marks the composable as focused for a11y. You'll notice in that link I sent that we only send the event if the composable gained focus. Nothing is done when it loses focus, and there's no notification sent when that happens. This goes back to the whole "focus is a transient thing" thing, so using state to model it is inherently weird. So really what this means is we need a recomposition where the focus is set to true, then we can disable it so that later when we want to pull focus again (say we dismiss the bottom sheet then call it back up) we can deliver that false -> true semantic property shift that triggers talkback to give us focus. So in the end we have
var isFocused by remember { mutableStateOf(false) }
// Focus MyComponent when the sheet opens or whatever
LaunchedEffect(sheetState.isOpenOrWhatever) {
  if(sheetState.isOpenOrWhatever) isFocused = true
}
// Clear isFocused flag next recomp so we can request focus again 
// the next time the sheet opens... or whatever
LaunchedEffect(isFocused) {
  if(isFocused) isFocused = false
}

MyComponent(
    modifier = Modifier.semantics { focused = isFocused }.clickable(onClick),
)
Feels a wee bit hacky but there's literally no other way to request a11y focus in compose so it's what we've got I guess.