Skip to content

Commit 4e293f2

Browse files
authored
Add isClearFocusOnMouseDownEnabled configuration in CfW (JetBrains#2781)
Introduces `isClearFocusOnMouseDownEnabled` to configure whether mouse clicks outside focused elements clear focus. Fixes https://youtrack.jetbrains.com/issue/CMP-9324 ## Testing Added additional tests to ensure that setting `isClearFocusOnMouseDownEnabled` affects the behaviour. ## Release Notes ### Features - Web - Add `isClearFocusOnMouseDownEnabled` in `ComposeViewportConfiguration` to configure the focus behaviour on mouse press
1 parent 5111bf3 commit 4e293f2

3 files changed

Lines changed: 103 additions & 0 deletions

File tree

compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/window/ComposeViewportConfiguration.web.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616

1717
package androidx.compose.ui.window
1818

19+
import androidx.compose.ui.ComposeUiFlags
1920
import androidx.compose.ui.ExperimentalComposeUiApi
21+
import androidx.compose.ui.isClearFocusOnMouseDownEnabled
2022

2123
/**
2224
* Configuration of [ComposeViewport] behavior.
@@ -35,4 +37,11 @@ class ComposeViewportConfiguration internal constructor() {
3537
*/
3638
@ExperimentalComposeUiApi
3739
var isA11YEnabled: Boolean = true
40+
41+
/**
42+
* Controls whether a mouse clicks on an unfocused element clears focus.
43+
* It's clearing focus on mouse down by default.
44+
*/
45+
@ExperimentalComposeUiApi
46+
var isClearFocusOnMouseDownEnabled: Boolean = ComposeUiFlags.isClearFocusOnMouseDownEnabled
3847
}

compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/window/ComposeWindowInternal.web.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,9 @@ internal class ComposeWindow(
315315
.startInputMethod(request)
316316
}
317317
}
318+
319+
override val isClearFocusOnMouseDownEnabled: Boolean
320+
get() = configuration.isClearFocusOnMouseDownEnabled
318321
}
319322

320323
private val skiaLayer: SkiaLayer = SkiaLayer().apply {

compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/input/TextFieldFocusTest.kt

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,43 @@
1616

1717
package androidx.compose.ui.input
1818

19+
import androidx.compose.foundation.layout.Box
1920
import androidx.compose.foundation.layout.Column
21+
import androidx.compose.foundation.layout.fillMaxWidth
22+
import androidx.compose.foundation.layout.size
23+
import androidx.compose.foundation.text.BasicTextField
2024
import androidx.compose.foundation.text.input.TextFieldLineLimits
2125
import androidx.compose.foundation.text.input.rememberTextFieldState
2226
import androidx.compose.material.TextField
27+
import androidx.compose.runtime.LaunchedEffect
2328
import androidx.compose.ui.Modifier
2429
import androidx.compose.ui.OnCanvasTests
30+
import androidx.compose.ui.background
2531
import androidx.compose.ui.events.keyEvent
2632
import androidx.compose.ui.focus.FocusRequester
2733
import androidx.compose.ui.focus.FocusState
2834
import androidx.compose.ui.focus.focusRequester
2935
import androidx.compose.ui.focus.onFocusChanged
36+
import androidx.compose.ui.graphics.Color
3037
import androidx.compose.ui.input.key.Key
38+
import androidx.compose.ui.platform.testTag
39+
import androidx.compose.ui.unit.dp
3140
import kotlin.test.Test
3241
import kotlin.test.assertEquals
3342
import kotlin.test.assertFalse
3443
import kotlin.test.assertNotNull
3544
import kotlin.test.assertTrue
45+
import kotlinx.coroutines.Dispatchers
46+
import kotlinx.coroutines.delay
47+
import kotlinx.coroutines.withContext
3648
import kotlinx.coroutines.yield
49+
import org.w3c.dom.HTMLDivElement
3750
import org.w3c.dom.HTMLInputElement
3851
import org.w3c.dom.events.Event
3952
import org.w3c.dom.events.KeyboardEvent
53+
import org.w3c.dom.events.MouseEvent
54+
import org.w3c.dom.pointerevents.PointerEvent
55+
import org.w3c.dom.pointerevents.PointerEventInit
4056

4157
class TextFieldFocusTest : OnCanvasTests {
4258

@@ -132,4 +148,79 @@ class TextFieldFocusTest : OnCanvasTests {
132148
assertTrue((lastKeydownEventOnRoot as KeyboardEvent).shiftKey)
133149
assertTrue(lastKeydownEventOnRoot!!.defaultPrevented)
134150
}
151+
152+
private fun mouseDownPointerEvent(
153+
x: Int, y: Int,
154+
): PointerEvent = PointerEvent(
155+
"pointerdown",
156+
PointerEventInit(
157+
clientX = x, clientY = y,
158+
button = 0, buttons = 1,
159+
pointerType = "mouse"
160+
)
161+
)
162+
163+
@Test
164+
fun mouseClickOutsideClearsFocusByDefault() = runApplicationTest {
165+
val focusRequester = FocusRequester()
166+
var focusState: FocusState? = null
167+
168+
createComposeWindow {
169+
Column(Modifier.size(300.dp, 400.dp)) {
170+
Box(Modifier.testTag("box").size(100.dp).background(Color.Gray))
171+
BasicTextField(
172+
state = rememberTextFieldState(),
173+
modifier = Modifier
174+
.testTag("textField")
175+
.focusRequester(focusRequester)
176+
.onFocusChanged {
177+
focusState = it
178+
}
179+
)
180+
LaunchedEffect(Unit) {
181+
focusRequester.requestFocus()
182+
}
183+
}
184+
}
185+
assertTrue(focusState!!.isFocused, "Expected to be focused after requestFocus")
186+
187+
dispatchEvents(mouseDownPointerEvent(50, 50))
188+
awaitIdle()
189+
assertFalse(focusState!!.isFocused, "Expected to lose focus after clicking outside")
190+
}
191+
192+
@Test
193+
fun mouseClickOutsideDoesntClearsFocusWhenDisabled() = runApplicationTest {
194+
val focusRequester = FocusRequester()
195+
var focusState: FocusState? = null
196+
197+
createComposeWindow(
198+
configure = {
199+
isClearFocusOnMouseDownEnabled = false
200+
}
201+
) {
202+
Column(Modifier.size(300.dp, 400.dp)) {
203+
Box(Modifier.testTag("box").size(100.dp).background(Color.Gray))
204+
BasicTextField(
205+
state = rememberTextFieldState(),
206+
modifier = Modifier
207+
.testTag("textField")
208+
.focusRequester(focusRequester)
209+
.onFocusChanged {
210+
focusState = it
211+
}
212+
)
213+
LaunchedEffect(Unit) {
214+
focusRequester.requestFocus()
215+
}
216+
}
217+
}
218+
assertTrue(focusState!!.isFocused, "Expected to be focused after requestFocus")
219+
220+
dispatchEvents(mouseDownPointerEvent(50, 50))
221+
awaitIdle()
222+
assertTrue(focusState!!.isFocused, "Expected to keep focus despite clicking outside")
223+
}
224+
225+
135226
}

0 commit comments

Comments
 (0)