From 1eda72d5f98018dba74cd94a00bd1b72e7f95439 Mon Sep 17 00:00:00 2001 From: David Perez Date: Thu, 8 Oct 2026 18:53:47 +0000 Subject: [PATCH] PM-44005: bug: Add focus state to hypertext links (#7455) --- .../ui/platform/base/util/SpanStyleUtil.kt | 15 +++++++++++++++ .../ui/platform/base/util/StringResExtensions.kt | 14 ++++++++++++++ .../components/text/BitwardenClickableText.kt | 15 +++++++++------ 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/base/util/SpanStyleUtil.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/base/util/SpanStyleUtil.kt index cbb4918f02..dbe93fba37 100644 --- a/ui/src/main/kotlin/com/bitwarden/ui/platform/base/util/SpanStyleUtil.kt +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/base/util/SpanStyleUtil.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextDecoration import com.bitwarden.ui.platform.theme.BitwardenTheme /** @@ -67,3 +68,17 @@ val bitwardenClickableTextSpanStyle: SpanStyle color = BitwardenTheme.colorScheme.text.interaction, textStyle = BitwardenTheme.typography.labelMedium, ) + +/** + * A [SpanStyle] representing "clickable" text in the focused state using the background alert + * color and the underlined label medium font. + */ +val bitwardenClickableTextFocusSpanStyle: SpanStyle + @Composable + @ReadOnlyComposable + get() = spanStyleOf( + color = BitwardenTheme.colorScheme.background.alert, + textStyle = BitwardenTheme.typography.labelMedium.copy( + textDecoration = TextDecoration.Underline, + ), + ) diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/base/util/StringResExtensions.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/base/util/StringResExtensions.kt index 2bdfbda55e..ba383c18ce 100644 --- a/ui/src/main/kotlin/com/bitwarden/ui/platform/base/util/StringResExtensions.kt +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/base/util/StringResExtensions.kt @@ -26,6 +26,8 @@ import androidx.core.text.getSpans * with the "emphasis" annotation. * @param linkHighlightStyle Style to apply to part of the resource that has been annotated with * the "link" annotation. + * @param linkHighlightFocusStyle Style to apply to part of the resource that has been annotated + * with the "link" annotation and is currently focused. * @param resources The resources used to access the strings. * @param onAnnotationClick Callback to invoke when a link annotation is clicked. Will pass back * the value of the annotation as a string to allow for delineation if there are multiple callbacks @@ -52,6 +54,7 @@ fun @receiver:StringRes Int.toAnnotatedString( style: SpanStyle = bitwardenDefaultSpanStyle, emphasisHighlightStyle: SpanStyle = bitwardenBoldSpanStyle, linkHighlightStyle: SpanStyle = bitwardenClickableTextSpanStyle, + linkHighlightFocusStyle: SpanStyle = bitwardenClickableTextFocusSpanStyle, resources: Resources = LocalResources.current, onAnnotationClick: ((annotationKey: String) -> Unit)? = null, ): AnnotatedString = remember( @@ -60,6 +63,7 @@ fun @receiver:StringRes Int.toAnnotatedString( style, emphasisHighlightStyle, linkHighlightStyle, + linkHighlightFocusStyle, onAnnotationClick, ) { val spannedString = try { @@ -73,6 +77,7 @@ fun @receiver:StringRes Int.toAnnotatedString( style = style, emphasisHighlightStyle = emphasisHighlightStyle, linkHighlightStyle = linkHighlightStyle, + linkHighlightFocusStyle = linkHighlightFocusStyle, onAnnotationClick = onAnnotationClick, ) } @@ -90,6 +95,8 @@ fun @receiver:StringRes Int.toAnnotatedString( * with the "emphasis" annotation. * @param linkHighlightStyle Style to apply to part of the resource that has been annotated with * the "link" annotation. + * @param linkHighlightFocusStyle Style to apply to part of the resource that has been annotated + * with the "link" annotation and is currently focused. * @param resources The resources used to access the strings. * @param onAnnotationClick Callback to invoke when a link annotation is clicked. Will pass back * the value of the annotation as a string to allow for delineation if there are multiple callbacks @@ -105,6 +112,7 @@ fun @receiver:PluralsRes Int.toAnnotatedPluralsString( style: SpanStyle = bitwardenDefaultSpanStyle, emphasisHighlightStyle: SpanStyle = bitwardenBoldSpanStyle, linkHighlightStyle: SpanStyle = bitwardenClickableTextSpanStyle, + linkHighlightFocusStyle: SpanStyle = bitwardenClickableTextFocusSpanStyle, resources: Resources = LocalResources.current, onAnnotationClick: ((annotationKey: String) -> Unit)? = null, ): AnnotatedString = remember( @@ -114,6 +122,7 @@ fun @receiver:PluralsRes Int.toAnnotatedPluralsString( style, emphasisHighlightStyle, linkHighlightStyle, + linkHighlightFocusStyle, onAnnotationClick, ) { val spannedString = try { @@ -129,6 +138,7 @@ fun @receiver:PluralsRes Int.toAnnotatedPluralsString( style = style, emphasisHighlightStyle = emphasisHighlightStyle, linkHighlightStyle = linkHighlightStyle, + linkHighlightFocusStyle = linkHighlightFocusStyle, onAnnotationClick = onAnnotationClick, ) } @@ -138,11 +148,13 @@ fun @receiver:PluralsRes Int.toAnnotatedPluralsString( * `arg` annotations with the corresponding [args] entry then applies emphasis and link styles * to the resulting [AnnotatedString]. */ +@Suppress("LongParameterList") private fun SpannedString.buildAnnotatedString( args: Array, style: SpanStyle, emphasisHighlightStyle: SpanStyle, linkHighlightStyle: SpanStyle, + linkHighlightFocusStyle: SpanStyle, onAnnotationClick: ((annotationKey: String) -> Unit)?, ): AnnotatedString { // The spannableBuilder is used to help parse through the annotations in the string resource. @@ -181,6 +193,8 @@ private fun SpannedString.buildAnnotatedString( tag = annotation.value.orEmpty(), styles = TextLinkStyles( style = linkHighlightStyle, + focusedStyle = linkHighlightFocusStyle, + hoveredStyle = linkHighlightFocusStyle, ), ) { onAnnotationClick?.invoke(annotation.value.orEmpty()) diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/text/BitwardenClickableText.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/text/BitwardenClickableText.kt index 6fe89d84ae..eb306a21ee 100644 --- a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/text/BitwardenClickableText.kt +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/text/BitwardenClickableText.kt @@ -1,6 +1,7 @@ package com.bitwarden.ui.platform.components.text import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -71,6 +72,7 @@ fun BitwardenClickableText( modifier = modifier .defaultMinSize(minHeight = 48.dp) .clip(shape = shape) + .focusable(enabled = isEnabled) .clickable( indication = ripple(color = BitwardenTheme.colorScheme.background.pressed), interactionSource = interactionSource, @@ -88,15 +90,16 @@ fun BitwardenClickableText( contentDescription = formattedAccessibilityString }, ) { + val foregroundColor = if (isEnabled) { + color + } else { + BitwardenTheme.colorScheme.filledButton.foregroundDisabled + } leadingIcon?.let { Icon( painter = leadingIcon, contentDescription = null, - tint = if (isEnabled) { - color - } else { - BitwardenTheme.colorScheme.filledButton.foregroundDisabled - }, + tint = foregroundColor, modifier = Modifier.size(size = 16.dp), ) Spacer(modifier = Modifier.width(width = 8.dp)) @@ -104,7 +107,7 @@ fun BitwardenClickableText( Text( text = label, textAlign = TextAlign.Start, - color = color, + color = foregroundColor, style = style, modifier = Modifier.semantics { hideFromAccessibility() }, )