mirror of
https://github.com/jlengrand/compose-multiplatform.git
synced 2026-03-10 08:11:20 +00:00
Added tabbing navigation tutorial. (#817)
* Added tabbing navigation tutorial.
This commit is contained in:
@@ -38,6 +38,7 @@ Preview functionality (check your application UI without building/running it) fo
|
||||
* [Top level windows management](tutorials/Window_API_new)
|
||||
* [Menu, tray, notifications](tutorials/Tray_Notifications_MenuBar_new)
|
||||
* [Keyboard support](tutorials/Keyboard)
|
||||
* [Tab focus navigation](tutorials/Tab_Navigation)
|
||||
* [Building native distribution](tutorials/Native_distributions_and_local_execution)
|
||||
* [Signing and notarization](tutorials/Signing_and_notarization_on_macOS)
|
||||
* [Swing interoperability](tutorials/Swing_Integration)
|
||||
|
||||
312
tutorials/Tab_Navigation/README.md
Normal file
312
tutorials/Tab_Navigation/README.md
Normal file
@@ -0,0 +1,312 @@
|
||||
# Tabbing navigation and keyboard focus
|
||||
|
||||
## What is covered
|
||||
|
||||
In this tutorial, we will show you how to use tabbing navigation between components via keyboard shortcuts `tab` and `shift + tab`.
|
||||
|
||||
## Default `Next/Previous` tabbing navigation
|
||||
|
||||
By default, `Next/Previous` tabbed navigation moves focus in composition order (in order of appearance), to see how this works, we can use some of the components that are already focusable by default:`TextField`, `OutlinedTextField`, `BasicTextField`, `CircularProgressIndicator`, `LinearProgressIndicator`.
|
||||
|
||||
```kotlin
|
||||
import androidx.compose.ui.window.application
|
||||
import androidx.compose.ui.window.Window
|
||||
import androidx.compose.ui.window.WindowState
|
||||
import androidx.compose.ui.window.WindowSize
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
fun main() = application {
|
||||
Window(
|
||||
state = WindowState(size = WindowSize(350.dp, 500.dp)),
|
||||
onCloseRequest = ::exitApplication
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(50.dp)
|
||||
) {
|
||||
for (x in 1..5) {
|
||||
val text = remember { mutableStateOf("") }
|
||||
OutlinedTextField(
|
||||
value = text.value,
|
||||
singleLine = true,
|
||||
onValueChange = { text.value = it }
|
||||
)
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
To make a non-focusable component focusable, you need to apply `Modifier.focusable()` modifier to the component.
|
||||
|
||||
```kotlin
|
||||
import androidx.compose.ui.window.application
|
||||
import androidx.compose.ui.window.Window
|
||||
import androidx.compose.ui.window.WindowState
|
||||
import androidx.compose.ui.window.WindowSize
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.lerp
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.input.key.KeyEventType
|
||||
import androidx.compose.ui.input.key.type
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
|
||||
fun main() = application {
|
||||
Window(
|
||||
state = WindowState(size = WindowSize(350.dp, 450.dp)),
|
||||
onCloseRequest = ::exitApplication
|
||||
) {
|
||||
MaterialTheme(
|
||||
colors = MaterialTheme.colors.copy(
|
||||
primary = Color(10, 132, 232),
|
||||
secondary = Color(150, 232, 150)
|
||||
)
|
||||
) {
|
||||
val clicks = remember { mutableStateOf(0) }
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(40.dp)
|
||||
) {
|
||||
Text(text = "Clicks: ${clicks.value}")
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
for (x in 1..5) {
|
||||
FocusableButton("Button $x", { clicks.value++ })
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun FocusableButton(
|
||||
text: String = "",
|
||||
onClick: () -> Unit = {},
|
||||
size: IntSize = IntSize(200, 35)
|
||||
) {
|
||||
val keyPressedState = remember { mutableStateOf(false) }
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val colors = ButtonDefaults.buttonColors(
|
||||
backgroundColor = if (interactionSource.collectIsFocusedAsState().value) {
|
||||
if (keyPressedState.value)
|
||||
lerp(MaterialTheme.colors.secondary, Color(64, 64, 64), 0.3f)
|
||||
else
|
||||
MaterialTheme.colors.secondary
|
||||
} else {
|
||||
MaterialTheme.colors.primary
|
||||
}
|
||||
)
|
||||
Button(
|
||||
onClick = onClick,
|
||||
interactionSource = interactionSource,
|
||||
modifier = Modifier.size(size.width.dp, size.height.dp)
|
||||
.onPreviewKeyEvent {
|
||||
if (
|
||||
it.key == Key.Enter ||
|
||||
it.key == Key.Spacebar
|
||||
) {
|
||||
when (it.type) {
|
||||
KeyEventType.KeyDown -> {
|
||||
keyPressedState.value = true
|
||||
}
|
||||
KeyEventType.KeyUp -> {
|
||||
keyPressedState.value = false
|
||||
onClick.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
.focusable(interactionSource = interactionSource),
|
||||
colors = colors
|
||||
) {
|
||||
Text(text = text)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Custom ordering
|
||||
To move focus in custom order we need to create a `FocusRequester` and apply the `Modifier.focusOrder` modifier to each component you want to navigate.
|
||||
|
||||
- `FocusRequester` sends requests to change focus.
|
||||
- `Modifier.focusOrder` is used to specify a custom focus traversal order.
|
||||
|
||||
In the example below, we simply create a `FocusRequester` list and create text fields for each `FocusRequester` in the list. Each text field sends a focus request to the previous and next text field in the list when using the `shift + tab` or `tab` keyboard shortcut in reverse order.
|
||||
|
||||
```kotlin
|
||||
import androidx.compose.ui.window.application
|
||||
import androidx.compose.ui.window.Window
|
||||
import androidx.compose.ui.window.WindowState
|
||||
import androidx.compose.ui.window.WindowSize
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusOrder
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
fun main() = application {
|
||||
Window(
|
||||
state = WindowState(size = WindowSize(350.dp, 500.dp)),
|
||||
onCloseRequest = ::exitApplication
|
||||
) {
|
||||
val itemsList = remember { List(5) { FocusRequester() } }
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(50.dp)
|
||||
) {
|
||||
itemsList.forEachIndexed { index, item ->
|
||||
val text = remember { mutableStateOf("") }
|
||||
OutlinedTextField(
|
||||
value = text.value,
|
||||
singleLine = true,
|
||||
onValueChange = { text.value = it },
|
||||
modifier = Modifier.focusOrder(item) {
|
||||
// reverse order
|
||||
next = if (index - 1 < 0) itemsList.last() else itemsList[index - 1]
|
||||
previous = if (index + 1 == itemsList.size) itemsList.first() else itemsList[index + 1]
|
||||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Making component focused
|
||||
|
||||
To make a component focused, we need to create a `FocusRequester` and apply the `Modifier.focusRequester` modifier to the component you want to focus on. With `FocusRequester`, we can request focus, as in the example below:
|
||||
|
||||
```kotlin
|
||||
import androidx.compose.ui.window.application
|
||||
import androidx.compose.ui.window.Window
|
||||
import androidx.compose.ui.window.WindowState
|
||||
import androidx.compose.ui.window.WindowSize
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
fun main() = application {
|
||||
Window(
|
||||
state = WindowState(size = WindowSize(350.dp, 450.dp)),
|
||||
onCloseRequest = ::exitApplication
|
||||
) {
|
||||
val buttonFocusRequester = remember { FocusRequester() }
|
||||
val textFieldFocusRequester = remember { FocusRequester() }
|
||||
val focusState = remember { mutableStateOf(false) }
|
||||
val text = remember { mutableStateOf("") }
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(50.dp)
|
||||
) {
|
||||
Button(
|
||||
onClick = {
|
||||
focusState.value = !focusState.value
|
||||
if (focusState.value) {
|
||||
textFieldFocusRequester.requestFocus()
|
||||
} else {
|
||||
buttonFocusRequester.requestFocus()
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.focusRequester(buttonFocusRequester)
|
||||
.focusable()
|
||||
) {
|
||||
Text(text = "Focus switcher")
|
||||
}
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
OutlinedTextField(
|
||||
value = text.value,
|
||||
singleLine = true,
|
||||
onValueChange = { text.value = it },
|
||||
modifier = Modifier
|
||||
.focusRequester(textFieldFocusRequester)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
BIN
tutorials/Tab_Navigation/default-tab-nav.gif
Normal file
BIN
tutorials/Tab_Navigation/default-tab-nav.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 373 KiB |
BIN
tutorials/Tab_Navigation/focus-switcher.gif
Normal file
BIN
tutorials/Tab_Navigation/focus-switcher.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 465 KiB |
BIN
tutorials/Tab_Navigation/focusable-button.gif
Normal file
BIN
tutorials/Tab_Navigation/focusable-button.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 398 KiB |
BIN
tutorials/Tab_Navigation/reverse-order.gif
Normal file
BIN
tutorials/Tab_Navigation/reverse-order.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 310 KiB |
Reference in New Issue
Block a user