1+ /*
2+ * Copyright 2025 The Android Open Source Project
3+ *
4+ * Licensed under the Apache License, Version 2.0 (the "License");
5+ * you may not use this file except in compliance with the License.
6+ * You may obtain a copy of the License at
7+ *
8+ * http://www.apache.org/licenses/LICENSE-2.0
9+ *
10+ * Unless required by applicable law or agreed to in writing, software
11+ * distributed under the License is distributed on an "AS IS" BASIS,
12+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+ * See the License for the specific language governing permissions and
14+ * limitations under the License.
15+ */
16+
17+ package androidx.compose.ui.input
18+
19+ import androidx.compose.foundation.layout.Column
20+ import androidx.compose.foundation.text.input.TextFieldLineLimits
21+ import androidx.compose.foundation.text.input.rememberTextFieldState
22+ import androidx.compose.material.TextField
23+ import androidx.compose.ui.Modifier
24+ import androidx.compose.ui.OnCanvasTests
25+ import androidx.compose.ui.events.keyEvent
26+ import androidx.compose.ui.focus.FocusRequester
27+ import androidx.compose.ui.focus.FocusState
28+ import androidx.compose.ui.focus.focusRequester
29+ import androidx.compose.ui.focus.onFocusChanged
30+ import androidx.compose.ui.input.key.Key
31+ import kotlin.test.Test
32+ import kotlin.test.assertEquals
33+ import kotlin.test.assertFalse
34+ import kotlin.test.assertNotNull
35+ import kotlin.test.assertTrue
36+ import kotlinx.coroutines.yield
37+ import org.w3c.dom.HTMLInputElement
38+ import org.w3c.dom.events.Event
39+ import org.w3c.dom.events.KeyboardEvent
40+
41+ class TextFieldFocusTest : OnCanvasTests {
42+
43+ @Test
44+ fun canMoveFocusForwardAndBackUsingTab () = runApplicationTest {
45+ val focusRequester = FocusRequester ()
46+
47+ suspend fun waitForSingleLineHtmlInput (): HTMLInputElement {
48+ while (true ) {
49+ val element = getShadowRoot().querySelector(" input" )
50+ if (element is HTMLInputElement ) {
51+ return element
52+ }
53+ yield ()
54+ }
55+ }
56+
57+ var firstTextFieldFocusState: FocusState ? = null
58+ var secondTextFieldFocusState: FocusState ? = null
59+
60+ createComposeWindow {
61+ Column {
62+ TextField (
63+ state = rememberTextFieldState(initialText = " Hello" ),
64+ modifier = Modifier
65+ .focusRequester(focusRequester)
66+ .onFocusChanged({
67+ firstTextFieldFocusState = it
68+ }),
69+ lineLimits = TextFieldLineLimits .SingleLine
70+ )
71+
72+ TextField (
73+ state = rememberTextFieldState(initialText = " World" ),
74+ lineLimits = TextFieldLineLimits .SingleLine ,
75+ modifier = Modifier .onFocusChanged({
76+ secondTextFieldFocusState = it
77+ })
78+ )
79+ }
80+ }
81+
82+ var lastKeydownEventOnRoot: Event ? = null
83+
84+ focusRequester.requestFocus()
85+
86+ val htmlInput1 = waitForSingleLineHtmlInput()
87+ assertNotNull(firstTextFieldFocusState)
88+ assertNotNull(secondTextFieldFocusState)
89+ assertEquals(true , firstTextFieldFocusState.isFocused)
90+ assertEquals(false , secondTextFieldFocusState.isFocused)
91+
92+ getShadowRoot().addEventListener(" keydown" , {
93+ lastKeydownEventOnRoot = it
94+ })
95+
96+ val tabKeyDown = keyEvent(
97+ key = " Tab" ,
98+ type = " keydown" ,
99+ keyCode = Key .Tab .keyCode.toInt(),
100+ code = " Tab"
101+ )
102+ htmlInput1.dispatchEvent(tabKeyDown)
103+ awaitAnimationFrame()
104+ assertNotNull(lastKeydownEventOnRoot)
105+ assertEquals(" Tab" , (lastKeydownEventOnRoot as KeyboardEvent ).key)
106+ assertFalse((lastKeydownEventOnRoot as KeyboardEvent ).shiftKey)
107+ assertTrue(lastKeydownEventOnRoot!! .defaultPrevented)
108+ lastKeydownEventOnRoot = null
109+
110+ assertEquals(false , firstTextFieldFocusState.isFocused)
111+ assertEquals(true , secondTextFieldFocusState.isFocused)
112+
113+ /* Now move focus back using Tab+Shift */
114+
115+ val htmlInput2 = waitForSingleLineHtmlInput()
116+ val tabKeyDownWithShift = keyEvent(
117+ key = " Tab" ,
118+ type = " keydown" ,
119+ keyCode = Key .Tab .keyCode.toInt(),
120+ code = " Tab" ,
121+ shiftKey = true
122+ )
123+
124+ htmlInput2.dispatchEvent(tabKeyDownWithShift)
125+ awaitAnimationFrame()
126+
127+ assertEquals(true , firstTextFieldFocusState.isFocused)
128+ assertEquals(false , secondTextFieldFocusState.isFocused)
129+
130+ assertNotNull(lastKeydownEventOnRoot)
131+ assertEquals(" Tab" , (lastKeydownEventOnRoot as KeyboardEvent ).key)
132+ assertTrue((lastKeydownEventOnRoot as KeyboardEvent ).shiftKey)
133+ assertTrue(lastKeydownEventOnRoot!! .defaultPrevented)
134+ }
135+ }
0 commit comments