rob42
06/11/2024, 11:40 AMLazyColumn
via Modifier.focusProperties { canFocus = false }
).
This is a "wontfix: by design" in compose, and arguably not a big deal on mobile, but it's a real problem on desktop for keyboard navigation and accessibility. People can tab into UI that should not be accessible based on the current state.
Is this something that compose desktop could handle differently? Or is there another way of preventing focus for a whole tree of composables?Alexander Maryanovsky
06/12/2024, 9:13 AMAlexander Maryanovsky
06/12/2024, 9:15 AMModifier.focusProperties { canFocus = false }
, by design, applies only to the element itself, not its children.rob42
06/14/2024, 11:06 AMrob42
06/14/2024, 11:06 AM/** Focusable prevents focus within its content by redirecting focus elsewhere immediately on focus */
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun Focusable(modifier: Modifier = Modifier, focusable: Boolean, content: @Composable () -> Unit) {
val focusManager = LocalFocusManager.current
var focusEntryDirection by remember { mutableStateOf<FocusDirection?>(null) }
Box(modifier = modifier
.onFocusEvent { event ->
if (focusable) return@onFocusEvent
if (!event.isFocused && !event.hasFocus) return@onFocusEvent
// The box is designed to have a single child, so moving focus in onFocusEvent *before* any children
// gain focus means navigation occurs relative to the outer box. Therefore, no focus anchors are needed.
when (focusEntryDirection) {
FocusDirection.Previous -> {
focusManager.moveFocus(FocusDirection.Previous)
}
FocusDirection.Next -> {
focusManager.moveFocus(FocusDirection.Next)
}
else -> {
// So far in testing, focus always comes from Previous/Next, unless it was manually requested
// via a focus requester on a child composable.
// May need to handle Up/Down/Left/Right too.
focusManager.clearFocus()
}
}
}
.focusProperties {
// This box (itself) shouldn't be focusable, but by default its children can be
canFocus = false
// To support disabling focus, accept the focus then immediately push it elsewhere in onFocusEvent
// - `enter = { Cancel }` would consume the focus attempt rather than skipping over this item
// - onFocusEvent API provides no way to know the entry direction, hence this hack. An alternative would
// be to detect the shift key, but that feels even hackier.
enter = {
focusEntryDirection = it
Default
}
}
.focusable()
) {
CompositionLocalProvider(LocalFocusability provides focusable) {
content()
}
}
}
val LocalFocusability = compositionLocalOf { true }