From fafffd2aebae1cac990ad990ce746eea64e2c8c4 Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Thu, 17 Oct 2024 09:32:24 -0700 Subject: [PATCH] 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 (cherry picked from commit 354a903ee1d093fd1bd7a33897975d8e073edfe5) --- .../ipn/ui/util/ClipboardValueView.kt | 67 ++++++++-------- .../ipn/ui/view/TailnetLockSetupView.kt | 76 ++++++++++--------- 2 files changed, 79 insertions(+), 64 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/ui/util/ClipboardValueView.kt b/android/src/main/java/com/tailscale/ipn/ui/util/ClipboardValueView.kt index 90e7b79..865282f 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/util/ClipboardValueView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/util/ClipboardValueView.kt @@ -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) + ) + } + ) +} \ No newline at end of file diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/TailnetLockSetupView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/TailnetLockSetupView.kt index d21e6af..bf3cb79 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/TailnetLockSetupView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/TailnetLockSetupView.kt @@ -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)) + } + } } } }