androboy
androboy

Reputation: 586

Jetpack Compose: TextField clickable does not work

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

Upvotes: 44

Views: 35487

Answers (7)

VishnuPrabhu
VishnuPrabhu

Reputation: 190

TextField(
    modifier = Modifier
        .fillMaxWidth()
        .clickable(onClick = onClick),
    enabled = false,
    value = currentSelection,
    onValueChange = { },
    label = {

    }
)

use the enabled = false and clickable modifier.

Upvotes: 2

Merry O
Merry O

Reputation: 351

The suggestion of setting the enabled attribute as false works, but this affects 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 = OutlinedTextFieldDefaults.colors().copy(
      disabledTextColor = MaterialTheme.colorScheme.onSurface,
      disabledIndicatorColor = MaterialTheme.colorScheme.outline,
      disabledPlaceholderColor = MaterialTheme.colorScheme.onSurfaceVariant,
      disabledLabelColor = MaterialTheme.colorScheme.onSurfaceVariant,
      //For Icons
      disabledLeadingIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
      disabledTrailingIconColor = MaterialTheme.colorScheme.onSurfaceVariant)
)

Upvotes: 23

Sergei S
Sergei S

Reputation: 3077

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 =
    this.then(
        composed {
            clickable(indication = null,
                interactionSource = remember { MutableInteractionSource() }) {
                onClick()
            }
        })

Upvotes: 30

Roshan
Roshan

Reputation: 519

val interactionSource = remember { MutableInteractionSource() }
val isPressed: Boolean by interactionSource.collectIsPressedAsState()

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

TextField(
value = textFieldValue,
onValueChange = onTextFieldChange,
interactionSource = interactionSource
)

Upvotes: 10

Gabriele Mariotti
Gabriele Mariotti

Reputation: 364730

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
                ))
        }
    }

Upvotes: 9

beigirad
beigirad

Reputation: 5734

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
                    }
                }
            }
        }
)

Upvotes: 53

rewgoes
rewgoes

Reputation: 754

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.

Upvotes: 2

Related Questions