Is there a way for a component up in hierarchy to ...
# compose
d
Is there a way for a component up in hierarchy to be notified when none of its not immediate nested children has focus? I.e. if I have deeply nested set of TextFields and want to do something when focus is lost? I tried adding onFocusEvent to this grand-parent component, but it receives
hasFocus = false
immediately after one of the children receives
hasFocus = true
z
Can you share some code?
That sounds like a bug though probably
r
Ok, I was able to reproduce this:
Copy code
@Composable
fun HasFocusSample(){
    Column(
        Modifier.onFocusChanged { println("hasFocus = ${it.hasFocus}") }
    ) {
        BasicTextField(value= "TextField1", onValueChange = {})
        BasicTextField(value= "TextField2", onValueChange = {})
    }
}
Initially
Copy code
hasFocus = false
After clicking TextField1
Copy code
hasFocus = true
After clicking TextField2
Copy code
hasFocus = false
hasFocus = true
I think you are wondering why hasFocus was set to false. This is because in this sample, the common focus parent for TextField1 and TextField2 are above the Column. So when TextField1 loses focus, there is no focus in the hierarchy, when TextField2 gains focus, focus is restored to the hierarchy. However, if you make the Column focusable, then hasFocus will stay true, as focus doesn't leave the sub-hierarchy:
Copy code
@Composable
fun HasFocusSample(){
    Column(
        Modifier
            .onFocusChanged { println("hasFocus = ${it.hasFocus}") }
            .focusable()
    ) {
        BasicTextField(value= "TextField1", onValueChange = {})
        BasicTextField(value= "TextField2", onValueChange = {})
    }
}
Now the initial state is
Copy code
hasFocus = false
After clicking TextField1
Copy code
hasFocus = true
After clicking TextField2
Copy code
// no change
So there are two ways you can proceed: Option 1. Don't react to the onFocusChanged event, instead use hoisted state in the logic where you need to know if Column has focus
Copy code
@Composable
fun HasFocusSample(){
    var columnHasFocus by remember { mutableStateOf(false) }
    Column(Modifier.onFocusChanged { columnHasFocus = it.hasFocus}) {
        BasicTextField(value= "TextField1", onValueChange = {})
        BasicTextField(value= "TextField2", onValueChange = {})
    }
}
Option 2: Keep focus within this hierarchy by making the Column focusable, so that you don't get the intermediate hasFocus= false
Copy code
@Composable
fun HasFocusSample(){
    Column(
        Modifier
            .onFocusChanged { it.hasFocus }
            .focusable()
    ) {
        BasicTextField(value= "TextField1", onValueChange = {})
        BasicTextField(value= "TextField2", onValueChange = {})
    }
}
If you don't want to actually make the Column focusable, but just want it to be a focus parent, you can use
Copy code
Modifier
   .focusProperties { canFocus = false }
   .focusable()
z
Huh, i wouldn’t think that changing focus directly from one sibling to another could cause the focus to temporarily jump to the parent. Is that intentional? Ok Ralston explained it to me.
onFocusChanged
merely received focus changed events directly from its children. LayoutNodes know nothing about focus, so there’s no concept of the
Column
having focused “children” when it doesn’t have the
focusable
modifier, because as far as focus is concerned
Column
doesn’t exist. So putting
onFocusChanged
on the column is basically just like putting it on both the children directly.
👍 1
r
Thanks for explaining, Zach. I filed https://issuetracker.google.com/208860711 to track this issue, and see if we can come up with a better way to handle this case.
d
Thanks for the detailed explanation! It's not enough for me to have
boolean
state, because I need something like
enum FocusState { Field1, Field2, Field3, None }
, so I have a setup where each individual field sets the state to the corresponding enum case while
onFocusChange
from parent sets
None
in case
hasFocus
is
false
. I didn't set
Column
to
focusable()
, so I've used Option 1 from your example, but somehow when clicking in 1st text field I observed this sequence
Field1
,
None
,
None
,
None
(several of `None`s, may be due to recompositions). So for me it's not behaving as you described,
hasFocus
somehow turns into
false
in parent's listener. Maybe it's my setup, I'll need to come up with a minimal example to check. It's
1.0.5
version of compose, didn't try on newer one.