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 1 month 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
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
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.width
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp
import com.tailscale.ipn.R
import com.tailscale.ipn.ui.theme.titledListItem
@Composable
fun ClipboardValueView(value: String, title: String? = null, subtitle: String? = null) {
val localClipboardManager = LocalClipboardManager.current
val modifier =
Modifier.focusable()
.clickable {
localClipboardManager.setText(AnnotatedString(value))
}
val isFocused = remember { mutableStateOf(false) }
val localClipboardManager = LocalClipboardManager.current
val interactionSource = remember { MutableInteractionSource() }
ListItem(
colors = MaterialTheme.colorScheme.titledListItem,
modifier = modifier,
overlineContent = title?.let { { Text(it, style = MaterialTheme.typography.titleMedium) } },
headlineContent = { Text(text = value, style = MaterialTheme.typography.bodyMedium) },
supportingContent =
subtitle?.let {
{
Text(
it,
modifier = Modifier.padding(top = 8.dp),
style = MaterialTheme.typography.bodyMedium)
}
},
trailingContent = {
Icon(
painterResource(R.drawable.clipboard),
stringResource(R.string.copy_to_clipboard),
modifier = Modifier.width(24.dp).height(24.dp))
})
}
ListItem(
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) } },
headlineContent = { Text(text = value, style = MaterialTheme.typography.bodyMedium) },
supportingContent = subtitle?.let {
{ Text(it, modifier = Modifier.padding(top = 8.dp), style = MaterialTheme.typography.bodyMedium) }
},
trailingContent = {
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
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.clickable
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.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.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
@ -18,7 +20,9 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@ -52,40 +56,44 @@ fun TailnetLockSetupView(
Scaffold(topBar = { Header(R.string.tailnet_lock, onBack = backToSettings) }) { innerPadding ->
LoadingIndicator.Wrap {
Column(
modifier =
Modifier.padding(innerPadding)
.focusable()
.verticalScroll(rememberScrollState())
.fillMaxSize()) {
ExplainerView()
LazyColumn(modifier = Modifier.padding(innerPadding).fillMaxSize()) {
item { ExplainerView() }
statusItems.forEach { statusItem ->
Lists.ItemDivider()
items(statusItems) { statusItem ->
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(
leadingContent = {
Icon(
painter = painterResource(id = statusItem.icon),
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant)
},
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))
item {
// Node key section
Lists.SectionDivider()
ClipboardValueView(
value = nodeKey,
title = stringResource(R.string.node_key),
subtitle = stringResource(R.string.node_key_explainer))
// Tailnet lock key
Lists.SectionDivider()
ClipboardValueView(
value = tailnetLockTlPubKey,
title = stringResource(R.string.tailnet_lock_key),
subtitle = stringResource(R.string.tailnet_lock_key_explainer))
}
// Tailnet lock key section
Lists.SectionDivider()
ClipboardValueView(
value = tailnetLockTlPubKey,
title = stringResource(R.string.tailnet_lock_key),
subtitle = stringResource(R.string.tailnet_lock_key_explainer))
}
}
}
}
}

Loading…
Cancel
Save