From 44f5f614b6706ca367bf09039804c1dc064fdc8d Mon Sep 17 00:00:00 2001 From: David Perez Date: Tue, 10 Mar 2026 15:53:16 -0500 Subject: [PATCH] PM-29871: bug: Add external link callouts (#6634) --- .../SetupBrowserAutofillScreen.kt | 1 + .../components/listitem/BitwardenListItem.kt | 9 ++++++-- .../platform/feature/search/SearchContent.kt | 1 + .../settings/autofill/AutoFillScreen.kt | 13 +++++++++-- .../browser/BrowserAutofillSettingsCard.kt | 6 +++++ .../flightrecorder/FlightRecorderScreen.kt | 1 + .../components/ImportLoginsInstructionStep.kt | 1 + .../feature/item/VaultItemLoginContent.kt | 5 +++- .../itemlisting/VaultItemListingContent.kt | 1 + .../model/ListingItemOverflowAction.kt | 23 +++++++++++++++++++ .../vault/feature/vault/VaultEntryListItem.kt | 1 + .../FlightRecorderScreenTest.kt | 2 +- .../vault/feature/item/VaultItemScreenTest.kt | 6 ++--- .../dropdown/BitwardenMultiSelectButton.kt | 3 +++ .../components/text/BitwardenClickableText.kt | 21 ++++++++++++++++- .../components/text/BitwardenHyperTextLink.kt | 13 ++++++++++- 16 files changed, 96 insertions(+), 11 deletions(-) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupBrowserAutofillScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupBrowserAutofillScreen.kt index 5e63b9547d..b805a93cbf 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupBrowserAutofillScreen.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/accountsetup/SetupBrowserAutofillScreen.kt @@ -169,6 +169,7 @@ private fun SetupBrowserAutofillContent( label = stringResource(id = BitwardenString.why_is_this_step_required), style = BitwardenTheme.typography.labelMedium, onClick = onWhyIsThisStepRequiredClick, + isExternalLink = true, modifier = Modifier .wrapContentWidth() .align(alignment = Alignment.CenterHorizontally) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/components/listitem/BitwardenListItem.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/components/listitem/BitwardenListItem.kt index 90d43b6f4d..560a177b69 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/components/listitem/BitwardenListItem.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/components/listitem/BitwardenListItem.kt @@ -18,6 +18,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -157,7 +159,9 @@ fun BitwardenListItem( selectionItems = { selectionDataList.forEach { itemData -> BitwardenBasicDialogRow( - modifier = Modifier.testTag("AlertSelectionOption"), + modifier = Modifier + .semantics { contentDescription = itemData.contentDescription } + .testTag(tag = "AlertSelectionOption"), text = itemData.text, onClick = { shouldShowDialog = false @@ -171,11 +175,12 @@ fun BitwardenListItem( } /** - * Wrapper for the an individual selection item's data. + * Wrapper for an individual selection item's data. */ data class SelectionItemData( val text: String, val onClick: () -> Unit, + val contentDescription: String = text, val testTag: String? = null, ) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/SearchContent.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/SearchContent.kt index 6b39a94c03..badfbf1000 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/SearchContent.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/SearchContent.kt @@ -140,6 +140,7 @@ fun SearchContent( .map { option -> SelectionItemData( text = option.title(), + contentDescription = option.contentDescription(), onClick = { when (option) { is ListingItemOverflowAction.SendAction.DeleteClick -> { diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreen.kt index 0d5fdbc1f9..816e4de0a3 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreen.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/AutoFillScreen.kt @@ -201,11 +201,20 @@ private fun AutoFillScreenContent( .padding(horizontal = 16.dp), ) Spacer(modifier = Modifier.height(height = 8.dp)) + val autofillServicesLabel = stringResource(id = BitwardenString.autofill_services) BitwardenSwitch( - label = stringResource(id = BitwardenString.autofill_services), + label = autofillServicesLabel, supportingText = stringResource( id = BitwardenString.autofill_services_explanation_long, ), + contentDescription = if (state.isAutoFillServicesEnabled) { + autofillServicesLabel + } else { + stringResource( + id = BitwardenString.external_link_format, + formatArgs = arrayOf(autofillServicesLabel), + ) + }, isChecked = state.isAutoFillServicesEnabled, onCheckedChange = autoFillHandlers.onAutofillServicesClick, cardStyle = CardStyle.Full, @@ -397,7 +406,7 @@ private fun AutofillCallToActionCard( trailingContent = { Icon( painter = rememberVectorPainter(BitwardenDrawable.ic_chevron_right), - contentDescription = null, + contentDescription = stringResource(id = BitwardenString.external_link), tint = BitwardenTheme.colorScheme.icon.primary, ) }, diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/browser/BrowserAutofillSettingsCard.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/browser/BrowserAutofillSettingsCard.kt index 415e238d15..4356a92921 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/browser/BrowserAutofillSettingsCard.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/autofill/browser/BrowserAutofillSettingsCard.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.bitwarden.ui.platform.base.util.cardStyle @@ -13,6 +14,7 @@ import com.bitwarden.ui.platform.base.util.standardHorizontalMargin import com.bitwarden.ui.platform.base.util.toListItemCardStyle import com.bitwarden.ui.platform.components.model.CardStyle import com.bitwarden.ui.platform.components.toggle.BitwardenSwitch +import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.platform.theme.BitwardenTheme import com.x8bit.bitwarden.data.autofill.model.browser.BrowserPackage import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.browser.model.BrowserAutofillSettingsOption @@ -44,6 +46,10 @@ fun BrowserAutofillSettingsCard( onCheckedChange = { onOptionClicked(option.browserPackage) }, + contentDescription = stringResource( + id = BitwardenString.external_link_format, + formatArgs = arrayOf(option.optionText()), + ), cardStyle = when { supportingText == null -> options.toListItemCardStyle(index = index) index == 0 -> CardStyle.Top(dividerPadding = 16.dp) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/FlightRecorderScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/FlightRecorderScreen.kt index 2cb81f142e..a8bdc912d4 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/FlightRecorderScreen.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/FlightRecorderScreen.kt @@ -163,6 +163,7 @@ private fun FlightRecorderContent( onClick = onHelpCenterClick, color = BitwardenTheme.colorScheme.text.secondary, style = BitwardenTheme.typography.bodySmall, + isExternalLink = true, modifier = Modifier .fillMaxWidth() .standardHorizontalMargin(), diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/importlogins/components/ImportLoginsInstructionStep.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/importlogins/components/ImportLoginsInstructionStep.kt index 99d59055bd..4a4e33b3e2 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/importlogins/components/ImportLoginsInstructionStep.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/importlogins/components/ImportLoginsInstructionStep.kt @@ -73,6 +73,7 @@ fun ImportLoginsInstructionStep( accessibilityString = stringResource(id = BitwardenString.import_help), onClick = onHelpClick, style = BitwardenTheme.typography.bodySmall, + isExternalLink = true, modifier = Modifier.standardHorizontalMargin(), ) Spacer(Modifier.height(24.dp)) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt index 188c1a6cf1..0f1d8998c3 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemLoginContent.kt @@ -515,7 +515,10 @@ private fun UriField( if (uriData.isLaunchable) { BitwardenStandardIconButton( vectorIconRes = BitwardenDrawable.ic_external_link, - contentDescription = stringResource(id = BitwardenString.launch), + contentDescription = stringResource( + id = BitwardenString.external_link_format, + formatArgs = arrayOf(stringResource(id = BitwardenString.launch)), + ), onClick = { onLaunchUriClick(uriData.uri) }, modifier = Modifier.testTag(tag = "LoginLaunchUriButton"), ) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingContent.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingContent.kt index c1ef1dd250..f2ddc02230 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingContent.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingContent.kt @@ -261,6 +261,7 @@ fun VaultItemListingContent( .map { option -> SelectionItemData( text = option.title(), + contentDescription = option.contentDescription(), onClick = { when (option) { is ListingItemOverflowAction.SendAction.DeleteClick -> { diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/model/ListingItemOverflowAction.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/model/ListingItemOverflowAction.kt index d7ae55f3a5..8c90d04592 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/model/ListingItemOverflowAction.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/model/ListingItemOverflowAction.kt @@ -18,6 +18,11 @@ sealed class ListingItemOverflowAction : Parcelable { */ abstract val title: Text + /** + * The content description of the option. + */ + abstract val contentDescription: Text + /** * Represents the send actions. */ @@ -31,6 +36,7 @@ sealed class ListingItemOverflowAction : Parcelable { val sendType: SendType, ) : SendAction() { override val title: Text get() = BitwardenString.view.asText() + override val contentDescription: Text get() = title } /** @@ -42,6 +48,7 @@ sealed class ListingItemOverflowAction : Parcelable { val sendType: SendType, ) : SendAction() { override val title: Text get() = BitwardenString.edit.asText() + override val contentDescription: Text get() = title } /** @@ -50,6 +57,7 @@ sealed class ListingItemOverflowAction : Parcelable { @Parcelize data class CopyUrlClick(val sendUrl: String) : SendAction() { override val title: Text get() = BitwardenString.copy_link.asText() + override val contentDescription: Text get() = title } /** @@ -58,6 +66,7 @@ sealed class ListingItemOverflowAction : Parcelable { @Parcelize data class ShareUrlClick(val sendUrl: String) : SendAction() { override val title: Text get() = BitwardenString.share_link.asText() + override val contentDescription: Text get() = title } /** @@ -66,6 +75,7 @@ sealed class ListingItemOverflowAction : Parcelable { @Parcelize data class RemovePasswordClick(val sendId: String) : SendAction() { override val title: Text get() = BitwardenString.remove_password.asText() + override val contentDescription: Text get() = title } /** @@ -74,6 +84,7 @@ sealed class ListingItemOverflowAction : Parcelable { @Parcelize data class DeleteClick(val sendId: String) : SendAction() { override val title: Text get() = BitwardenString.delete.asText() + override val contentDescription: Text get() = title } } @@ -97,6 +108,7 @@ sealed class ListingItemOverflowAction : Parcelable { override val requiresPasswordReprompt: Boolean, ) : VaultAction() { override val title: Text get() = BitwardenString.view.asText() + override val contentDescription: Text get() = title } /** @@ -109,6 +121,7 @@ sealed class ListingItemOverflowAction : Parcelable { override val requiresPasswordReprompt: Boolean, ) : VaultAction() { override val title: Text get() = BitwardenString.edit.asText() + override val contentDescription: Text get() = title } /** @@ -118,6 +131,7 @@ sealed class ListingItemOverflowAction : Parcelable { data class CopyUsernameClick(val username: String) : VaultAction() { override val title: Text get() = BitwardenString.copy_username.asText() override val requiresPasswordReprompt: Boolean get() = false + override val contentDescription: Text get() = title } /** @@ -129,6 +143,7 @@ sealed class ListingItemOverflowAction : Parcelable { override val requiresPasswordReprompt: Boolean, ) : VaultAction() { override val title: Text get() = BitwardenString.copy_password.asText() + override val contentDescription: Text get() = title } /** @@ -140,6 +155,7 @@ sealed class ListingItemOverflowAction : Parcelable { override val requiresPasswordReprompt: Boolean, ) : VaultAction() { override val title: Text get() = BitwardenString.copy_totp.asText() + override val contentDescription: Text get() = title } /** @@ -151,6 +167,7 @@ sealed class ListingItemOverflowAction : Parcelable { override val requiresPasswordReprompt: Boolean, ) : VaultAction() { override val title: Text get() = BitwardenString.copy_number.asText() + override val contentDescription: Text get() = title } /** @@ -162,6 +179,7 @@ sealed class ListingItemOverflowAction : Parcelable { override val requiresPasswordReprompt: Boolean, ) : VaultAction() { override val title: Text get() = BitwardenString.copy_security_code.asText() + override val contentDescription: Text get() = title } /** @@ -173,6 +191,7 @@ sealed class ListingItemOverflowAction : Parcelable { override val requiresPasswordReprompt: Boolean, ) : VaultAction() { override val title: Text get() = BitwardenString.copy_notes.asText() + override val contentDescription: Text get() = title } /** @@ -182,6 +201,8 @@ sealed class ListingItemOverflowAction : Parcelable { data class LaunchClick(val url: String) : VaultAction() { override val title: Text get() = BitwardenString.launch.asText() override val requiresPasswordReprompt: Boolean get() = false + override val contentDescription: Text + get() = BitwardenString.external_link_format.asText(title) } /** @@ -191,6 +212,7 @@ sealed class ListingItemOverflowAction : Parcelable { data class ArchiveClick(val cipherId: String) : VaultAction() { override val title: Text get() = BitwardenString.archive_verb.asText() override val requiresPasswordReprompt: Boolean get() = true + override val contentDescription: Text get() = title } /** @@ -200,6 +222,7 @@ sealed class ListingItemOverflowAction : Parcelable { data class UnarchiveClick(val cipherId: String) : VaultAction() { override val title: Text get() = BitwardenString.unarchive.asText() override val requiresPasswordReprompt: Boolean get() = true + override val contentDescription: Text get() = title } } } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultEntryListItem.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultEntryListItem.kt index 30b61fe3c7..cc95628c05 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultEntryListItem.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultEntryListItem.kt @@ -53,6 +53,7 @@ fun VaultEntryListItem( .map { option -> SelectionItemData( text = option.title(), + contentDescription = option.contentDescription(), onClick = { onOverflowOptionClick(option) }, ) } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/FlightRecorderScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/FlightRecorderScreenTest.kt index 0c7f28f71a..c5a1727eed 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/FlightRecorderScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/FlightRecorderScreenTest.kt @@ -77,7 +77,7 @@ class FlightRecorderScreenTest : BitwardenComposeTest() { composeTestRule .onNodeWithText(text = "Bitwarden help center", substring = true) .performScrollTo() - .performCustomAccessibilityAction(label = "Bitwarden help center") + .performCustomAccessibilityAction(label = "Bitwarden help center, External link") verify(exactly = 1) { viewModel.trySendAction(FlightRecorderAction.HelpCenterClick) } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt index 5c267a53b2..9d2842c88c 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/item/VaultItemScreenTest.kt @@ -2367,7 +2367,7 @@ class VaultItemScreenTest : BitwardenComposeTest() { composeTestRule .onNodeWithTextAfterScroll(uriData.uri) .onChildren() - .filterToOne(hasContentDescription("Launch")) + .filterToOne(hasContentDescription("Launch, External link")) .assertIsDisplayed() mutableStateFlow.update { currentState -> @@ -2379,7 +2379,7 @@ class VaultItemScreenTest : BitwardenComposeTest() { composeTestRule .onNodeWithTextAfterScroll(uriData.uri) .onSiblings() - .filterToOne(hasContentDescription("Launch")) + .filterToOne(hasContentDescription("Launch, External link")) .assertDoesNotExist() } @@ -2437,7 +2437,7 @@ class VaultItemScreenTest : BitwardenComposeTest() { composeTestRule .onNodeWithTextAfterScroll(uriData.uri) .onChildren() - .filterToOne(hasContentDescription("Launch")) + .filterToOne(hasContentDescription("Launch, External link")) .performClick() verify { diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/dropdown/BitwardenMultiSelectButton.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/dropdown/BitwardenMultiSelectButton.kt index 1ac08336aa..bfed456b2a 100644 --- a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/dropdown/BitwardenMultiSelectButton.kt +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/dropdown/BitwardenMultiSelectButton.kt @@ -17,6 +17,8 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.bitwarden.ui.platform.base.util.nullableTestTag @@ -202,6 +204,7 @@ fun ColumnScope.BitwardenMultiSelectDialogContent( is MultiSelectOption.Header -> { Column( modifier = Modifier + .semantics(mergeDescendants = true) { heading() } .nullableTestTag(tag = it.testTag) .padding(horizontal = 16.dp) .fillMaxWidth(), 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 70fb85b770..265c020891 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 @@ -20,12 +20,17 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.hideFromAccessibility +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.bitwarden.ui.platform.components.button.BitwardenTextButton +import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.platform.theme.BitwardenTheme /** @@ -34,6 +39,7 @@ import com.bitwarden.ui.platform.theme.BitwardenTheme * @param label The label for the button. * @param onClick The callback when the button is clicked. * @param modifier The [Modifier] to be applied to the button. + * @param isExternalLink Indicates that this button launches an external link. */ @Composable fun BitwardenClickableText( @@ -41,12 +47,21 @@ fun BitwardenClickableText( onClick: () -> Unit, style: TextStyle, modifier: Modifier = Modifier, + isExternalLink: Boolean = false, leadingIcon: Painter? = null, innerPadding: PaddingValues = PaddingValues(vertical = 4.dp, horizontal = 16.dp), isEnabled: Boolean = true, cornerSize: Dp = 28.dp, color: Color = BitwardenTheme.colorScheme.text.interaction, ) { + val formattedAccessibilityString = if (isExternalLink) { + stringResource( + id = BitwardenString.external_link_format, + formatArgs = arrayOf(label), + ) + } else { + label + } Row( verticalAlignment = Alignment.CenterVertically, modifier = modifier @@ -58,7 +73,10 @@ fun BitwardenClickableText( enabled = isEnabled, onClick = onClick, ) - .padding(paddingValues = innerPadding), + .padding(paddingValues = innerPadding) + .semantics(mergeDescendants = true) { + contentDescription = formattedAccessibilityString + }, ) { leadingIcon?.let { Icon( @@ -78,6 +96,7 @@ fun BitwardenClickableText( textAlign = TextAlign.Start, color = color, style = style, + modifier = Modifier.semantics { hideFromAccessibility() }, ) } } diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/text/BitwardenHyperTextLink.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/text/BitwardenHyperTextLink.kt index f5e7672111..b60cbc1295 100644 --- a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/text/BitwardenHyperTextLink.kt +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/text/BitwardenHyperTextLink.kt @@ -5,6 +5,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.CustomAccessibilityAction import androidx.compose.ui.semantics.customActions import androidx.compose.ui.semantics.semantics @@ -12,6 +13,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign import com.bitwarden.ui.platform.base.util.annotatedStringResource import com.bitwarden.ui.platform.base.util.spanStyleOf +import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.platform.theme.BitwardenTheme /** @@ -29,7 +31,16 @@ fun BitwardenHyperTextLink( modifier: Modifier = Modifier, style: TextStyle = BitwardenTheme.typography.labelMedium, color: Color = BitwardenTheme.colorScheme.text.secondary, + isExternalLink: Boolean = false, ) { + val formattedAccessibilityString = if (isExternalLink) { + stringResource( + id = BitwardenString.external_link_format, + formatArgs = arrayOf(accessibilityString), + ) + } else { + accessibilityString + } Text( text = annotatedStringResource( id = annotatedResId, @@ -48,7 +59,7 @@ fun BitwardenHyperTextLink( .semantics { customActions = listOf( CustomAccessibilityAction( - label = accessibilityString, + label = formattedAccessibilityString, action = { onClick() true