android: make tailnet lock setup view focusable and clickable (#544)

-use a shared InteractionSource for focusing and clicking to ensure they rely on the same state and to coordinate so that visual feedback is shown on scroll without affecting the click InteractionSource
-use LocalIndication to ensure that the click interaction maintains the visual feedback when combined with focusable
-use onFocusChanged to explicitly track the focus state

Updates tailscale/corp#21737

Signed-off-by: kari-ts <kari@tailscale.com>
pull/545/head
kari-ts 2 weeks ago committed by GitHub
parent 6ec54234ef
commit 354a903ee1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -3,52 +3,59 @@
package com.tailscale.ipn.ui.util package com.tailscale.ipn.ui.util
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.height import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.tailscale.ipn.R import com.tailscale.ipn.R
import com.tailscale.ipn.ui.theme.titledListItem
@Composable @Composable
fun ClipboardValueView(value: String, title: String? = null, subtitle: String? = null) { fun ClipboardValueView(value: String, title: String? = null, subtitle: String? = null) {
val isFocused = remember { mutableStateOf(false) }
val localClipboardManager = LocalClipboardManager.current val localClipboardManager = LocalClipboardManager.current
val modifier = val interactionSource = remember { MutableInteractionSource() }
Modifier.focusable()
.clickable {
localClipboardManager.setText(AnnotatedString(value))
}
ListItem( ListItem(
colors = MaterialTheme.colorScheme.titledListItem, modifier = Modifier
modifier = modifier, .focusable(interactionSource = interactionSource)
.onFocusChanged { focusState -> isFocused.value = focusState.isFocused }
.clickable(
interactionSource = interactionSource,
indication = LocalIndication.current
) { localClipboardManager.setText(AnnotatedString(value)) }
.background(
if (isFocused.value) MaterialTheme.colorScheme.primary.copy(alpha = 0.12f)
else Color.Transparent
),
overlineContent = title?.let { { Text(it, style = MaterialTheme.typography.titleMedium) } }, overlineContent = title?.let { { Text(it, style = MaterialTheme.typography.titleMedium) } },
headlineContent = { Text(text = value, style = MaterialTheme.typography.bodyMedium) }, headlineContent = { Text(text = value, style = MaterialTheme.typography.bodyMedium) },
supportingContent = supportingContent = subtitle?.let {
subtitle?.let { { Text(it, modifier = Modifier.padding(top = 8.dp), style = MaterialTheme.typography.bodyMedium) }
{
Text(
it,
modifier = Modifier.padding(top = 8.dp),
style = MaterialTheme.typography.bodyMedium)
}
}, },
trailingContent = { trailingContent = {
Icon( Icon(
painterResource(R.drawable.clipboard), painterResource(R.drawable.clipboard),
stringResource(R.string.copy_to_clipboard), contentDescription = stringResource(R.string.copy_to_clipboard),
modifier = Modifier.width(24.dp).height(24.dp)) modifier = Modifier.size(24.dp)
}) )
}
)
} }

@ -3,13 +3,15 @@
package com.tailscale.ipn.ui.view package com.tailscale.ipn.ui.view
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.text.ClickableText
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -18,7 +20,9 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -52,18 +56,19 @@ fun TailnetLockSetupView(
Scaffold(topBar = { Header(R.string.tailnet_lock, onBack = backToSettings) }) { innerPadding -> Scaffold(topBar = { Header(R.string.tailnet_lock, onBack = backToSettings) }) { innerPadding ->
LoadingIndicator.Wrap { LoadingIndicator.Wrap {
Column( LazyColumn(modifier = Modifier.padding(innerPadding).fillMaxSize()) {
modifier = item { ExplainerView() }
Modifier.padding(innerPadding)
.focusable()
.verticalScroll(rememberScrollState())
.fillMaxSize()) {
ExplainerView()
statusItems.forEach { statusItem ->
Lists.ItemDivider()
items(statusItems) { statusItem ->
val interactionSource = remember { MutableInteractionSource() }
ListItem( ListItem(
modifier =
Modifier.focusable(
interactionSource = interactionSource)
.clickable(
interactionSource = interactionSource,
indication = LocalIndication.current
) {},
leadingContent = { leadingContent = {
Icon( Icon(
painter = painterResource(id = statusItem.icon), painter = painterResource(id = statusItem.icon),
@ -72,14 +77,16 @@ fun TailnetLockSetupView(
}, },
headlineContent = { Text(stringResource(statusItem.title)) }) headlineContent = { Text(stringResource(statusItem.title)) })
} }
// Node key
item {
// Node key section
Lists.SectionDivider() Lists.SectionDivider()
ClipboardValueView( ClipboardValueView(
value = nodeKey, value = nodeKey,
title = stringResource(R.string.node_key), title = stringResource(R.string.node_key),
subtitle = stringResource(R.string.node_key_explainer)) subtitle = stringResource(R.string.node_key_explainer))
// Tailnet lock key // Tailnet lock key section
Lists.SectionDivider() Lists.SectionDivider()
ClipboardValueView( ClipboardValueView(
value = tailnetLockTlPubKey, value = tailnetLockTlPubKey,
@ -88,6 +95,7 @@ fun TailnetLockSetupView(
} }
} }
} }
}
} }
@Composable @Composable

Loading…
Cancel
Save