21

for some reason Compose TextField's click listener does not work for me.

@Composable
    private fun ExposedDropdown(
        modifier: Modifier,
        list: List<String>,
        priority: Int
    ) {
        var expanded by remember { mutableStateOf(false) }
        Column(modifier) {
            OutlinedTextField(
                value = list[priority],
                onValueChange = { },
                readOnly = true,
                singleLine = true,
                label = { Text(stringResource(id = R.string.status)) },
                modifier = Modifier
                    .fillMaxWidth()
                    .clickable { Timber.i("Not working :(") }
                    .onFocusChanged { if (it.isFocused) expanded = !expanded },
                trailingIcon = {
                    Icon(
                        imageVector = Icons.Outlined.ArrowDropDown,
                        contentDescription = null,
                        modifier = Modifier
                            .clickable { expanded = !expanded }
                            .padding(16.dp)
                    )
                }
            )
            DropdownMenu(
                expanded = expanded,
                onDismissRequest = { expanded = false }
            ) {
                list.forEach { label ->
                    DropdownMenuItem(onClick = {
                        viewModel.setPriority(list.indexOf(label))
                        expanded = false
                    }) {
                        Text(text = label)
                    }
                }
            }
        }
    }

As you can see I come up with bad solution using onFocusChanged but it does not work well.

For those who need context, I'm trying to do ExposedDropdown but I want it to open when I click anywhere on TextField

Gabriele Mariotti
  • 320,139
  • 94
  • 887
  • 841
androboy
  • 356
  • 1
  • 2
  • 9

7 Answers7

27

Another possible workaround can be this:

import kotlinx.coroutines.flow.collect

TextField(
    value = ...,
    onValueChange = { ... },
    interactionSource = remember { MutableInteractionSource() }
        .also { interactionSource ->
            LaunchedEffect(interactionSource) {
                interactionSource.interactions.collect {
                    if (it is PressInteraction.Release) {
                        // works like onClick
                    }
                }
            }
        }
)

beigirad
  • 4,986
  • 2
  • 29
  • 52
  • 5
    so others dont stumble on this, make sure you use `import kotlinx.coroutines.flow.collect ` as the sourse of collect else you will get a weird exception. apparently someone at kotlin decided to have a second method named collect in the same library XD – quealegriamasalegre Mar 07 '22 at 14:48
  • finally somebody gave the correct answer with check `if (it is PressInteraction.Release) {`, there are many answers where it's just `val isPressed: Boolean by interactionSource.collectIsPressedAsState()` which fires as soon as you tap and don't event release your finger – user924 Mar 23 '23 at 18:30
  • 1
    worth to note: does not force to set `enabled = false` to receive callback. – Irwin Nawrocki Apr 04 '23 at 11:29
  • 1
    How to add ripple effect? – Risal Fajar Amiyardi Aug 11 '23 at 15:05
20

with compose 1.0.2 it works by default. Need to add line enabled = false

Example

@Composable
fun SelectableTextField(
    modifier: Modifier = Modifier,
    textValue: String,
    onClick: () -> Unit
) {
    TextField(
        value = textValue,
        onValueChange = {},
        modifier = modifier
            .fillMaxWidth()
            .clickable { onClick() },
        enabled = false
    )
}

to remove ripple effect, use such extension

inline fun Modifier.noRippleClickable(crossinline onClick: () -> Unit): Modifier =
    composed {
        clickable(indication = null,
            interactionSource = remember { MutableInteractionSource() }) {
            onClick()
        }
    }
Sergei S
  • 2,553
  • 27
  • 36
  • 2
    Why do we have to have `enabled = false` to have `clickable` work? – dazza5000 Aug 05 '22 at 04:17
  • 2
    Found this bug that outlines the reason https://issuetracker.google.com/issues/172154008 - Please star/comment if you think this should work differently – dazza5000 Aug 05 '22 at 04:19
9
val interactionSource = remember { MutableInteractionSource() }
val isPressed: Boolean by interactionSource.collectIsPressedAsState()

LaunchedEffect(isPressed){
    if (isPressed) {
        // Click action
    }
}

TextField(
value = textFieldValue,
onValueChange = onTextFieldChange,
interactionSource = interactionSource
)
Roshan
  • 369
  • 4
  • 9
  • how to test this? The functionality is there but `performClick` won't trigger the click callback :/ – Song Aug 19 '22 at 23:23
  • 1
    This could run the isPressed handler any number of times. It should use LaunchedEffect e.g. `LaunchedEffect(isPressed) { if(isPressed) { /* Click Action */ }}` – Mike Dawson Nov 09 '22 at 08:29
  • 1
    this is not really correct, it fires as soon as you tap on the field and don't event remove your finger, it should be called only after you release the finger, you can't even scroll normally in the column when you have many text fields with it – user924 Mar 23 '23 at 18:23
8

The suggestion of setting the enabled attribute as false works, but this effects the style. If you want it to visually match an enabled TextField then set the colors attribute accordingly:

OutlinedTextField(
   value = "Example",
   enabled = false,
   onValueChange = {},
   modifier = Modifier.clickable { doSomeBehavior() },
   colors = TextFieldDefaults.outlinedTextFieldColors(
      disabledTextColor = MaterialTheme.colorScheme.onSurface,
      disabledBorderColor = MaterialTheme.colorScheme.outline,
      disabledPlaceholderColor = MaterialTheme.colorScheme.onSurfaceVariant,
      disabledLabelColor = MaterialTheme.colorScheme.onSurfaceVariant,
      //For Icons
      disabledLeadingIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
      disabledTrailingIconColor = MaterialTheme.colorScheme.onSurfaceVariant)
)
Noah O
  • 91
  • 1
  • 6
7

The clickable modifier currently (1.0.0-beta08) doesn't work with a TextField.

It is a workaround, not a real solution.
Since your TextField is readonly, you can wrap the OutlinedTextField with in a Box using a second Box to handle the click.

 val focusRequester = FocusRequester.createRefs()
 val interactionSource = remember { MutableInteractionSource() }

 Box() {
        OutlinedTextField(
          //...your code
          modifier = Modifier
            .fillMaxWidth()
            .focusRequester(focusRequester)
        }
                    
        if (!expanded) {
            Box(modifier = Modifier
                .matchParentSize()
                .clickable(
                    onClick = {
                        expanded = !expanded
                        focusRequester.requestFocus() //to give the focus to the TextField
                              },
                    interactionSource = interactionSource,
                    indication = null //to avoid the ripple on the Box
                ))
        }
    }
Codeversed
  • 9,287
  • 3
  • 43
  • 42
Gabriele Mariotti
  • 320,139
  • 94
  • 887
  • 841
2

Here is a possible solution. I've created a collectClickAsState() composable based on androidx.compose.foundation.interaction.collectIsPressedAsState:

@Composable
fun CustomTextField(
    value: String,
    onValueChange: (String) -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    onClick: (() -> Unit)? = null,
) {
    val onClickSource = remember { MutableInteractionSource() }

    if (onClick != null) {
        if (onClickSource.collectClickAsState().value) {
            onClick.invoke()
        }
    }

    TextField(
        value = value,
        onValueChange = onValueChange,
        interactionSource = onClickSource,
        enabled = enabled,
        readOnly = onClick != null,
        modifier = modifier
            // add clickable to work with talkback/accessibility
            .clickable(enabled = enabled) { onClick?.invoke() },
    )
}

@Composable
fun InteractionSource.collectClickAsState(): State<Boolean> {
    val onClick = remember { mutableStateOf(false) }
    LaunchedEffect(this) {
        var wasPressed = false
        interactions.collect { interaction ->
            when (interaction) {
                is PressInteraction.Press -> {
                    wasPressed = true
                }
                is PressInteraction.Release -> {
                    if (wasPressed) onClick.value = true
                    wasPressed = false
                }
                is PressInteraction.Cancel -> {
                    wasPressed = false
                }
            }
            // reset state with some delay otherwise it can re-emit the previous state
            delay(10L)
            onClick.value = false
        }
    }
    return onClick
}

With this solution, the text field is still focusable, the text is selectable and it will use the correct UI enabled state.

rewgoes
  • 656
  • 2
  • 9
  • 23
1
TextField(
        modifier = Modifier
            .fillMaxWidth()
            .**clickable**(onClick = onClick),
        enabled = false,
        value = currentSelection,
        onValueChange = { },
        label = {
            
        }
    )

use the enabled = false and clickable modifier.