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>
(cherry picked from commit 354a903ee1)
pull/554/head
kari-ts 1 year ago committed by Andrea Gottardo
parent a4ba359ea9
commit fafffd2aeb

@ -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 localClipboardManager = LocalClipboardManager.current val isFocused = remember { mutableStateOf(false) }
val modifier = val localClipboardManager = LocalClipboardManager.current
Modifier.focusable() val interactionSource = remember { MutableInteractionSource() }
.clickable {
localClipboardManager.setText(AnnotatedString(value))
}
ListItem( ListItem(
colors = MaterialTheme.colorScheme.titledListItem, modifier = Modifier
modifier = modifier, .focusable(interactionSource = interactionSource)
overlineContent = title?.let { { Text(it, style = MaterialTheme.typography.titleMedium) } }, .onFocusChanged { focusState -> isFocused.value = focusState.isFocused }
headlineContent = { Text(text = value, style = MaterialTheme.typography.bodyMedium) }, .clickable(
supportingContent = interactionSource = interactionSource,
subtitle?.let { indication = LocalIndication.current
{ ) { localClipboardManager.setText(AnnotatedString(value)) }
Text( .background(
it, if (isFocused.value) MaterialTheme.colorScheme.primary.copy(alpha = 0.12f)
modifier = Modifier.padding(top = 8.dp), else Color.Transparent
style = MaterialTheme.typography.bodyMedium) ),
} overlineContent = title?.let { { Text(it, style = MaterialTheme.typography.titleMedium) } },
}, headlineContent = { Text(text = value, style = MaterialTheme.typography.bodyMedium) },
trailingContent = { supportingContent = subtitle?.let {
Icon( { Text(it, modifier = Modifier.padding(top = 8.dp), style = MaterialTheme.typography.bodyMedium) }
painterResource(R.drawable.clipboard), },
stringResource(R.string.copy_to_clipboard), trailingContent = {
modifier = Modifier.width(24.dp).height(24.dp)) Icon(
}) painterResource(R.drawable.clipboard),
contentDescription = stringResource(R.string.copy_to_clipboard),
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,40 +56,44 @@ 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 -> items(statusItems) { statusItem ->
Lists.ItemDivider() val interactionSource = remember { MutableInteractionSource() }
ListItem(
modifier =
Modifier.focusable(
interactionSource = interactionSource)
.clickable(
interactionSource = interactionSource,
indication = LocalIndication.current
) {},
leadingContent = {
Icon(
painter = painterResource(id = statusItem.icon),
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant)
},
headlineContent = { Text(stringResource(statusItem.title)) })
}
ListItem( item {
leadingContent = { // Node key section
Icon( Lists.SectionDivider()
painter = painterResource(id = statusItem.icon), ClipboardValueView(
contentDescription = null, value = nodeKey,
tint = MaterialTheme.colorScheme.onSurfaceVariant) title = stringResource(R.string.node_key),
}, subtitle = stringResource(R.string.node_key_explainer))
headlineContent = { Text(stringResource(statusItem.title)) })
}
// Node key
Lists.SectionDivider()
ClipboardValueView(
value = nodeKey,
title = stringResource(R.string.node_key),
subtitle = stringResource(R.string.node_key_explainer))
// Tailnet lock key // Tailnet lock key section
Lists.SectionDivider() Lists.SectionDivider()
ClipboardValueView( ClipboardValueView(
value = tailnetLockTlPubKey, value = tailnetLockTlPubKey,
title = stringResource(R.string.tailnet_lock_key), title = stringResource(R.string.tailnet_lock_key),
subtitle = stringResource(R.string.tailnet_lock_key_explainer)) subtitle = stringResource(R.string.tailnet_lock_key_explainer))
} }
}
} }
} }
} }

Loading…
Cancel
Save