Merge pull request #5017 from element-hq/feature/bma/a11y/sessionVerification

[a11y] Improve session verification screens
This commit is contained in:
Benoit Marty
2025-07-17 15:45:32 +02:00
committed by GitHub
31 changed files with 123 additions and 209 deletions

View File

@@ -29,13 +29,13 @@ import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.analytics.api.AnalyticsOptInEvents
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.organisms.InfoListItem
import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.background.OnboardingBackground
import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.components.ClickableLinkText
import io.element.android.libraries.designsystem.components.PageTitle
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.text.buildAnnotatedStringWithStyledPart
@@ -89,10 +89,10 @@ private fun AnalyticsOptInHeader(
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
PageTitle(
modifier = Modifier.padding(top = 60.dp, bottom = 12.dp),
IconTitleSubtitleMolecule(
modifier = Modifier.padding(top = 60.dp, bottom = 28.dp),
title = stringResource(id = R.string.screen_analytics_prompt_title, state.applicationName),
subtitle = stringResource(id = R.string.screen_analytics_prompt_help_us_improve),
subTitle = stringResource(id = R.string.screen_analytics_prompt_help_us_improve),
iconStyle = BigIcon.Style.Default(CompoundIcons.Chart())
)
if (state.hasPolicyLink) {

View File

@@ -31,10 +31,10 @@ import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.ftue.impl.R
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.background.OnboardingBackground
import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.components.PageTitle
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
@@ -59,7 +59,7 @@ fun NotificationsOptInView(
.statusBarsPadding()
.fillMaxSize(),
background = { OnboardingBackground() },
header = { NotificationsOptInHeader(modifier = Modifier.padding(top = 60.dp, bottom = 12.dp)) },
header = { NotificationsOptInHeader(modifier = Modifier.padding(top = 60.dp, bottom = 28.dp)) },
footer = { NotificationsOptInFooter(state) },
) {
NotificationsOptInContent()
@@ -70,10 +70,10 @@ fun NotificationsOptInView(
private fun NotificationsOptInHeader(
modifier: Modifier = Modifier,
) {
PageTitle(
IconTitleSubtitleMolecule(
modifier = modifier,
title = stringResource(R.string.screen_notification_optin_title),
subtitle = stringResource(R.string.screen_notification_optin_subtitle),
subTitle = stringResource(R.string.screen_notification_optin_subtitle),
iconStyle = BigIcon.Style.Default(CompoundIcons.NotificationsSolid()),
)
}

View File

@@ -24,9 +24,9 @@ import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.ftue.impl.R
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.components.PageTitle
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
@@ -65,10 +65,11 @@ fun ChooseSelfVerificationModeView(
)
},
header = {
PageTitle(
IconTitleSubtitleMolecule(
modifier = Modifier.padding(bottom = 16.dp),
iconStyle = BigIcon.Style.Default(CompoundIcons.LockSolid()),
title = stringResource(id = R.string.screen_identity_confirmation_title),
subtitle = stringResource(id = R.string.screen_identity_confirmation_subtitle)
subTitle = stringResource(id = R.string.screen_identity_confirmation_subtitle)
)
},
footer = {

View File

@@ -36,5 +36,9 @@ data class IncomingVerificationState(
data object Canceled : Step
data object Completed : Step
data object Failure : Step
val isTimeLimited: Boolean
get() = this is Initial ||
this is Verifying
}
}

View File

@@ -8,6 +8,7 @@
package io.element.android.features.verifysession.impl.incoming
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
@@ -19,6 +20,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.ProgressBarRangeInfo
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.focused
import androidx.compose.ui.semantics.progressBarRangeInfo
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
@@ -30,9 +36,9 @@ import io.element.android.features.verifysession.impl.incoming.ui.SessionDetails
import io.element.android.features.verifysession.impl.ui.VerificationBottomMenu
import io.element.android.features.verifysession.impl.ui.VerificationContentVerifying
import io.element.android.features.verifysession.impl.ui.VerificationUserProfileContent
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.components.PageTitle
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@@ -140,10 +146,26 @@ private fun IncomingVerificationHeader(step: Step, request: VerificationRequest.
}
Step.Failure -> R.string.screen_session_verification_request_failure_subtitle
}
PageTitle(
val timeLimitMessage = if (step.isTimeLimited) {
stringResource(CommonStrings.a11y_time_limited_action_required)
} else {
""
}
IconTitleSubtitleMolecule(
modifier = Modifier
.padding(bottom = 16.dp)
.semantics(mergeDescendants = true) {
contentDescription = timeLimitMessage
focused = true
if (iconStyle == BigIcon.Style.Loading) {
// Same code than Modifier.progressSemantics()
progressBarRangeInfo = ProgressBarRangeInfo.Indeterminate
}
}
.focusable(),
iconStyle = iconStyle,
title = stringResource(id = titleTextId),
subtitle = stringResource(id = subtitleTextId)
subTitle = stringResource(id = subtitleTextId),
)
}
@@ -187,7 +209,9 @@ private fun ContentInitial(
}
is VerificationRequest.Incoming.User -> {
Column(
modifier = Modifier.fillMaxWidth().padding(top = 24.dp),
modifier = Modifier
.fillMaxWidth()
.padding(top = 24.dp),
) {
VerificationUserProfileContent(
userId = request.details.senderProfile.userId,

View File

@@ -18,6 +18,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.verifysession.impl.R
@@ -46,7 +47,8 @@ fun SessionDetailsView(
color = ElementTheme.colors.borderDisabled,
shape = RoundedCornerShape(8.dp)
)
.padding(24.dp),
.padding(24.dp)
.semantics(mergeDescendants = true) {},
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Row(
@@ -76,6 +78,7 @@ fun SessionDetailsView(
label = stringResource(CommonStrings.common_device_id),
text = deviceId.value,
modifier = Modifier.weight(5f),
spellText = true,
)
}
}

View File

@@ -29,5 +29,11 @@ data class OutgoingVerificationState(
data class Verifying(val data: SessionVerificationData, val state: AsyncData<Unit>) : Step
data object Completed : Step
data object Exit : Step
val isTimeLimited: Boolean
get() = this is Initial ||
this is AwaitingOtherDeviceResponse ||
this is Ready ||
this is Verifying
}
}

View File

@@ -9,6 +9,7 @@ package io.element.android.features.verifysession.impl.outgoing
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
@@ -22,6 +23,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.ProgressBarRangeInfo
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.focused
import androidx.compose.ui.semantics.progressBarRangeInfo
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
@@ -31,9 +37,9 @@ import io.element.android.features.verifysession.impl.outgoing.OutgoingVerificat
import io.element.android.features.verifysession.impl.ui.VerificationBottomMenu
import io.element.android.features.verifysession.impl.ui.VerificationContentVerifying
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.components.PageTitle
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@@ -180,11 +186,26 @@ private fun OutgoingVerificationHeader(step: Step, request: VerificationRequest.
}
is Step.Exit -> return
}
PageTitle(
val timeLimitMessage = if (step.isTimeLimited) {
stringResource(CommonStrings.a11y_time_limited_action_required)
} else {
""
}
IconTitleSubtitleMolecule(
modifier = Modifier
.padding(bottom = 16.dp)
.semantics(mergeDescendants = true) {
contentDescription = timeLimitMessage
focused = true
if (iconStyle == BigIcon.Style.Loading) {
// Same code than Modifier.progressSemantics()
progressBarRangeInfo = ProgressBarRangeInfo.Indeterminate
}
}
.focusable(),
iconStyle = iconStyle,
title = stringResource(id = titleTextId),
subtitle = stringResource(id = subtitleTextId)
subTitle = stringResource(id = subtitleTextId),
)
}

View File

@@ -24,6 +24,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@@ -39,14 +41,20 @@ internal fun VerificationContentVerifying(
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier.fillMaxSize().padding(bottom = 20.dp),
modifier = modifier
.fillMaxSize()
.padding(bottom = 20.dp),
contentAlignment = Alignment.Center
) {
when (data) {
is SessionVerificationData.Decimals -> {
val text = data.decimals.joinToString(separator = " - ") { it.toString() }
val text = data.decimals.joinToString(separator = " - ")
Text(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.semantics {
contentDescription = data.decimals.joinToString()
},
text = text,
style = ElementTheme.typography.fontHeadingLgBold,
color = ElementTheme.colors.textPrimary,
@@ -57,7 +65,9 @@ internal fun VerificationContentVerifying(
// We want each row to have up to 4 emojis
val rows = data.emojis.chunked(4)
Column(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.semantics(mergeDescendants = true) {},
verticalArrangement = Arrangement.spacedBy(40.dp),
) {
rows.forEach { emojis ->

View File

@@ -10,14 +10,26 @@ package io.element.android.libraries.designsystem.atomic.molecules
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.theme.components.Text
/**
* Display a label and a text in a column.
* @param label the label to display
* @param text the text to display
* @param modifier the modifier to apply to this layout
* @param spellText if true, the text will be spelled out in the content description for accessibility.
* Useful for deviceId for instance, that the screen reader will read as a list of letters instead of trying to read a
* word of random characters.
*/
@Composable
fun TextWithLabelMolecule(
label: String,
text: String,
modifier: Modifier = Modifier,
spellText: Boolean = false,
) {
Column(modifier = modifier) {
Text(
@@ -26,6 +38,11 @@ fun TextWithLabelMolecule(
color = ElementTheme.colors.textSecondary,
)
Text(
modifier = Modifier.semantics {
if (spellText) {
contentDescription = text.toList().joinToString()
}
},
text = text,
style = ElementTheme.typography.fontBodyMdRegular,
color = ElementTheme.colors.textPrimary,

View File

@@ -22,8 +22,8 @@ import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.components.PageTitle
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@@ -69,9 +69,10 @@ fun FlowStepPage(
)
},
header = {
PageTitle(
IconTitleSubtitleMolecule(
modifier = Modifier.padding(bottom = 16.dp),
title = title,
subtitle = subTitle,
subTitle = subTitle,
iconStyle = iconStyle,
)
},

View File

@@ -1,126 +0,0 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.designsystem.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextButton
/**
* Compound component that displays a big icon, a title, an optional subtitle and an optional call to action component.
*
* @param title the title to display
* @param iconStyle the style of the [BigIcon] to display
* @param modifier the modifier to apply to this layout
* @param subtitle the optional subtitle to display. It defaults to `null`
* @param callToAction the optional call to action component to display. It defaults to `null`
*/
@Composable
fun PageTitle(
title: AnnotatedString,
iconStyle: BigIcon.Style,
modifier: Modifier = Modifier,
subtitle: AnnotatedString? = null,
callToAction: @Composable (() -> Unit)? = null,
) {
Column(
modifier = modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
BigIcon(style = iconStyle)
Column(
modifier = Modifier.padding(vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
modifier = Modifier.fillMaxWidth(),
text = title,
style = ElementTheme.typography.fontHeadingMdBold,
color = ElementTheme.colors.textPrimary,
textAlign = TextAlign.Center,
)
subtitle?.let {
Text(
modifier = Modifier.fillMaxWidth(),
text = it,
style = ElementTheme.typography.fontBodyMdRegular,
color = ElementTheme.colors.textSecondary,
textAlign = TextAlign.Center,
)
}
}
callToAction?.invoke()
}
}
/**
* Compound component that displays a big icon, a title, an optional subtitle and an optional call to action component.
*
* @param title the title to display
* @param iconStyle the style of the [BigIcon] to display
* @param modifier the modifier to apply to this layout
* @param subtitle the optional subtitle to display. It defaults to `null`
* @param callToAction the optional call to action component to display. It defaults to `null`
*/
@Composable
fun PageTitle(
title: String,
iconStyle: BigIcon.Style,
modifier: Modifier = Modifier,
subtitle: String? = null,
callToAction: @Composable (() -> Unit)? = null,
) = PageTitle(
title = AnnotatedString(title),
iconStyle = iconStyle,
modifier = modifier,
subtitle = subtitle?.let { AnnotatedString(it) },
callToAction = callToAction
)
@PreviewsDayNight
@Composable
internal fun PageTitleWithIconFullPreview(@PreviewParameter(BigIconStyleProvider::class) style: BigIcon.Style) {
ElementPreview {
PageTitle(
modifier = Modifier.padding(top = 24.dp),
title = AnnotatedString("Headline"),
subtitle = AnnotatedString("Description goes here"),
iconStyle = style,
callToAction = {
TextButton(text = "Learn more", onClick = {})
}
)
}
}
@PreviewsDayNight
@Composable
internal fun PageTitleWithIconMinimalPreview() {
ElementPreview {
PageTitle(
modifier = Modifier.padding(top = 24.dp),
title = "Headline",
iconStyle = BigIcon.Style.Default(CompoundIcons.CheckCircleSolid()),
)
}
}

View File

@@ -11,7 +11,9 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -28,8 +30,8 @@ import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.components.PageTitle
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button
@@ -57,14 +59,15 @@ fun MediaDeleteConfirmationBottomSheet(
.fillMaxWidth()
.padding(horizontal = 16.dp),
) {
PageTitle(
IconTitleSubtitleMolecule(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp, horizontal = 8.dp),
title = stringResource(R.string.screen_media_browser_delete_confirmation_title),
iconStyle = BigIcon.Style.Default(CompoundIcons.Delete(), useCriticalTint = true),
subtitle = stringResource(R.string.screen_media_browser_delete_confirmation_subtitle),
subTitle = stringResource(R.string.screen_media_browser_delete_confirmation_subtitle),
)
Spacer(modifier = Modifier.height(16.dp))
MediaRow(
modifier = Modifier
.fillMaxWidth()

View File

@@ -44,9 +44,9 @@ import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.background.OnboardingBackground
import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.components.PageTitle
import io.element.android.libraries.designsystem.components.async.AsyncFailure
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreview
@@ -472,14 +472,14 @@ private fun EmptyContent(
modifier = Modifier.fillMaxSize(),
) {
OnboardingBackground()
PageTitle(
IconTitleSubtitleMolecule(
modifier = Modifier
.fillMaxWidth()
.padding(top = 44.dp)
.padding(24.dp),
title = stringResource(titleRes),
iconStyle = BigIcon.Style.Default(icon),
subtitle = stringResource(subtitleRes),
subTitle = stringResource(subtitleRes),
)
}
}

View File

@@ -78,8 +78,6 @@ class KonsistPreviewTest {
"MessagesReactionButtonAddPreview",
"MessagesReactionButtonExtraPreview",
"MessagesViewWithIdentityChangePreview",
"PageTitleWithIconFullPreview",
"PageTitleWithIconMinimalPreview",
"PendingMemberRowWithLongNamePreview",
"PinUnlockViewInAppPreview",
"PollAnswerViewDisclosedNotSelectedPreview",