JetBrains / compose-multiplatform

Compose Multiplatform, a modern UI framework for Kotlin that makes building performant and beautiful user interfaces easy and enjoyable.
https://jetbrains.com/lp/compose-multiplatform
Apache License 2.0
15.35k stars 1.12k forks source link

Clicking a clickable component inside another disabled clickable component throws IllegalStateException #5064

Open mgroth0 opened 4 days ago

mgroth0 commented 4 days ago

Describe the bug

When a clickable component is contained inside of another clickable component it can cause an error.

Affected platforms

Versions

To Reproduce

I am including two reproducers. One in which I use the clickable modifier, and another with Button components.

Reproducer 1

  1. Run this code
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application

fun main() {
    application {
        Window(
            onCloseRequest = ::exitApplication
        ) {
            Box(
                Modifier.clickable(
                    enabled = false
                ) {
                }
            ) {
                Box(
                    Modifier
                        .fillMaxSize()
                        .clickable {
                        }
                )
            }
        }
    }
}
  1. Click the window anywhere
  2. Observe error

Reproducer 2

import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application

fun main() {
    application {
        Window(
            onCloseRequest = ::exitApplication
        ) {
            Button(
                enabled = false,
                onClick = {},
                content = {
                    Button(
                        onClick = {
                        }
                    ) {
                        Text("Click")
                    }
                }
            )
        }
    }
}

Context

I am trying to create a custom tab pane. The tab should be a clickable element, so that the user can select a tab. The currently selected tab has its clickable modifier disabled because it is already selected. Each tab also has an "x" icon for removing the tab. Basically, just like the tabs in any web browser which also have a clickable "x" inside of the clickable tab itself.

Expected behavior

No exception to be thrown. Or if an exception must be thrown, make it more informative and actionable.

Observed behavior

An exception is thrown with a popup message. Note the exception includes only internal stack trace elements.

This exception is thrown. Click here to expand and see the full stack trace.

```log Exception in thread "AWT-EventQueue-0 @coroutine#64" java.lang.IllegalStateException: Cannot obtain node coordinator. Is the Modifier.Node attached? at androidx.compose.ui.internal.InlineClassHelperKt.throwIllegalStateExceptionForNullCheck(InlineClassHelper.kt:30) at androidx.compose.ui.node.DelegatableNodeKt.requireLayoutNode(DelegatableNode.kt:1389) at androidx.compose.ui.node.DelegatableNodeKt.requireOwner(DelegatableNode.kt:336) at androidx.compose.ui.Modifier$Node.getCoroutineScope(Modifier.kt:198) at androidx.compose.foundation.FocusableNode.onFocusEvent(Focusable.kt:227) at androidx.compose.foundation.AbstractClickableNode.onFocusEvent(Clickable.kt:1102) at androidx.compose.ui.focus.FocusEventModifierNodeKt.refreshFocusEventNodes(FocusEventModifierNode.kt:68) at androidx.compose.ui.focus.FocusTransactionsKt.performRequestFocus(FocusTransactions.kt:82) at androidx.compose.ui.focus.FocusTransactionsKt.requestFocus-Mxy_nc0(FocusTransactions.kt:50) at androidx.compose.ui.focus.FocusTransactionsKt.requestFocus(FocusTransactions.kt:43) at androidx.compose.ui.focus.FocusRequesterModifierNodeKt.requestFocus(FocusRequesterModifierNode.kt:43) at androidx.compose.foundation.ClickableKt.requestFocusWhenInMouseInputMode(Clickable.kt:1302) at androidx.compose.foundation.ClickableKt.access$requestFocusWhenInMouseInputMode(Clickable.kt:1) at androidx.compose.foundation.ClickableNode$clickPointerInput$2.invokeSuspend(Clickable.kt:638) at androidx.compose.foundation.ClickableNode$clickPointerInput$2.invoke-d-4ec7I(Clickable.kt) at androidx.compose.foundation.ClickableNode$clickPointerInput$2.invoke(Clickable.kt) at androidx.compose.foundation.gestures.TapGestureDetectorKt$detectTapAndPress$2$1$2.invokeSuspend(TapGestureDetector.kt:244) at _COROUTINE._BOUNDARY._(CoroutineDebugging.kt:42) at androidx.compose.foundation.AbstractClickableNode$onPointerEvent$3.invokeSuspend(Clickable.kt:1042) at androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNodeImpl$onPointerEvent$1.invokeSuspend(SuspendingPointerInputFilter.kt:622) Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [androidx.compose.ui.scene.ComposeContainer$DesktopCoroutineExceptionHandler@124995dc, androidx.compose.runtime.BroadcastFrameClock@33358fcb, CoroutineId(64), "coroutine#64":StandaloneCoroutine{Cancelling}@32ee92be, FlushCoroutineDispatcher@20dbd708] Caused by: java.lang.IllegalStateException: Cannot obtain node coordinator. Is the Modifier.Node attached? at androidx.compose.ui.internal.InlineClassHelperKt.throwIllegalStateExceptionForNullCheck(InlineClassHelper.kt:30) at androidx.compose.ui.node.DelegatableNodeKt.requireLayoutNode(DelegatableNode.kt:1389) at androidx.compose.ui.node.DelegatableNodeKt.requireOwner(DelegatableNode.kt:336) at androidx.compose.ui.Modifier$Node.getCoroutineScope(Modifier.kt:198) at androidx.compose.foundation.FocusableNode.onFocusEvent(Focusable.kt:227) at androidx.compose.foundation.AbstractClickableNode.onFocusEvent(Clickable.kt:1102) at androidx.compose.ui.focus.FocusEventModifierNodeKt.refreshFocusEventNodes(FocusEventModifierNode.kt:68) at androidx.compose.ui.focus.FocusTransactionsKt.performRequestFocus(FocusTransactions.kt:82) at androidx.compose.ui.focus.FocusTransactionsKt.requestFocus-Mxy_nc0(FocusTransactions.kt:50) at androidx.compose.ui.focus.FocusTransactionsKt.requestFocus(FocusTransactions.kt:43) at androidx.compose.ui.focus.FocusRequesterModifierNodeKt.requestFocus(FocusRequesterModifierNode.kt:43) at androidx.compose.foundation.ClickableKt.requestFocusWhenInMouseInputMode(Clickable.kt:1302) at androidx.compose.foundation.ClickableKt.access$requestFocusWhenInMouseInputMode(Clickable.kt:1) at androidx.compose.foundation.ClickableNode$clickPointerInput$2.invokeSuspend(Clickable.kt:638) at androidx.compose.foundation.ClickableNode$clickPointerInput$2.invoke-d-4ec7I(Clickable.kt) at androidx.compose.foundation.ClickableNode$clickPointerInput$2.invoke(Clickable.kt) at androidx.compose.foundation.gestures.TapGestureDetectorKt$detectTapAndPress$2$1$2.invokeSuspend(TapGestureDetector.kt:244) at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:104) at androidx.compose.ui.platform.FlushCoroutineDispatcher$dispatch$2$1.invoke(FlushCoroutineDispatcher.skiko.kt:63) at androidx.compose.ui.platform.FlushCoroutineDispatcher$dispatch$2$1.invoke(FlushCoroutineDispatcher.skiko.kt:58) at androidx.compose.ui.platform.FlushCoroutineDispatcher.performRun(FlushCoroutineDispatcher.skiko.kt:102) at androidx.compose.ui.platform.FlushCoroutineDispatcher.access$performRun(FlushCoroutineDispatcher.skiko.kt:37) at androidx.compose.ui.platform.FlushCoroutineDispatcher$dispatch$2.invokeSuspend(FlushCoroutineDispatcher.skiko.kt:58) at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:104) at java.desktop/java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:318) at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:773) at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:720) at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:714) at java.base/java.security.AccessController.executePrivileged(AccessController.java:778) at java.base/java.security.AccessController.doPrivileged(AccessController.java:400) at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:87) at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:742) at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:203) at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:124) at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:113) at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:109) at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101) at java.desktop/java.awt.EventDispatchThread.run(EventDispatchThread.java:90) ```

igordmn commented 3 days ago

Thanks!

Reproduced on 1.7.0-alpha01, Desktop.

Isn't reproduced on 1.6.10 or Android.