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
commit c40badec65
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.compound.tokens.generated.CompoundIcons
import io.element.android.features.analytics.api.AnalyticsOptInEvents 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.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.InfoListItem
import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism import io.element.android.libraries.designsystem.atomic.organisms.InfoListOrganism
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.background.OnboardingBackground import io.element.android.libraries.designsystem.background.OnboardingBackground
import io.element.android.libraries.designsystem.components.BigIcon import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.components.ClickableLinkText 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.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.text.buildAnnotatedStringWithStyledPart import io.element.android.libraries.designsystem.text.buildAnnotatedStringWithStyledPart
@ -89,10 +89,10 @@ private fun AnalyticsOptInHeader(
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
PageTitle( IconTitleSubtitleMolecule(
modifier = Modifier.padding(top = 60.dp, bottom = 12.dp), modifier = Modifier.padding(top = 60.dp, bottom = 28.dp),
title = stringResource(id = R.string.screen_analytics_prompt_title, state.applicationName), 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()) iconStyle = BigIcon.Style.Default(CompoundIcons.Chart())
) )
if (state.hasPolicyLink) { 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.compound.tokens.generated.CompoundIcons
import io.element.android.features.ftue.impl.R 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.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.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.background.OnboardingBackground import io.element.android.libraries.designsystem.background.OnboardingBackground
import io.element.android.libraries.designsystem.components.BigIcon 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.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarSize
@ -59,7 +59,7 @@ fun NotificationsOptInView(
.statusBarsPadding() .statusBarsPadding()
.fillMaxSize(), .fillMaxSize(),
background = { OnboardingBackground() }, 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) }, footer = { NotificationsOptInFooter(state) },
) { ) {
NotificationsOptInContent() NotificationsOptInContent()
@ -70,10 +70,10 @@ fun NotificationsOptInView(
private fun NotificationsOptInHeader( private fun NotificationsOptInHeader(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
PageTitle( IconTitleSubtitleMolecule(
modifier = modifier, modifier = modifier,
title = stringResource(R.string.screen_notification_optin_title), 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()), 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.compound.tokens.generated.CompoundIcons
import io.element.android.features.ftue.impl.R 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.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.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.components.BigIcon 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.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Button
@ -65,10 +65,11 @@ fun ChooseSelfVerificationModeView(
) )
}, },
header = { header = {
PageTitle( IconTitleSubtitleMolecule(
modifier = Modifier.padding(bottom = 16.dp),
iconStyle = BigIcon.Style.Default(CompoundIcons.LockSolid()), iconStyle = BigIcon.Style.Default(CompoundIcons.LockSolid()),
title = stringResource(id = R.string.screen_identity_confirmation_title), 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 = { footer = {

View file

@ -36,5 +36,9 @@ data class IncomingVerificationState(
data object Canceled : Step data object Canceled : Step
data object Completed : Step data object Completed : Step
data object Failure : 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 package io.element.android.features.verifysession.impl.incoming
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -19,6 +20,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource 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.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp 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.VerificationBottomMenu
import io.element.android.features.verifysession.impl.ui.VerificationContentVerifying import io.element.android.features.verifysession.impl.ui.VerificationContentVerifying
import io.element.android.features.verifysession.impl.ui.VerificationUserProfileContent 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.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.components.BigIcon 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.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight 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 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, iconStyle = iconStyle,
title = stringResource(id = titleTextId), title = stringResource(id = titleTextId),
subtitle = stringResource(id = subtitleTextId) subTitle = stringResource(id = subtitleTextId),
) )
} }
@ -187,7 +209,9 @@ private fun ContentInitial(
} }
is VerificationRequest.Incoming.User -> { is VerificationRequest.Incoming.User -> {
Column( Column(
modifier = Modifier.fillMaxWidth().padding(top = 24.dp), modifier = Modifier
.fillMaxWidth()
.padding(top = 24.dp),
) { ) {
VerificationUserProfileContent( VerificationUserProfileContent(
userId = request.details.senderProfile.userId, 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.features.verifysession.impl.R import io.element.android.features.verifysession.impl.R
@ -46,7 +47,8 @@ fun SessionDetailsView(
color = ElementTheme.colors.borderDisabled, color = ElementTheme.colors.borderDisabled,
shape = RoundedCornerShape(8.dp) shape = RoundedCornerShape(8.dp)
) )
.padding(24.dp), .padding(24.dp)
.semantics(mergeDescendants = true) {},
verticalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp),
) { ) {
Row( Row(
@ -76,6 +78,7 @@ fun SessionDetailsView(
label = stringResource(CommonStrings.common_device_id), label = stringResource(CommonStrings.common_device_id),
text = deviceId.value, text = deviceId.value,
modifier = Modifier.weight(5f), 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 class Verifying(val data: SessionVerificationData, val state: AsyncData<Unit>) : Step
data object Completed : Step data object Completed : Step
data object Exit : 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.activity.compose.BackHandler
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -22,6 +23,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource 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.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme 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.VerificationBottomMenu
import io.element.android.features.verifysession.impl.ui.VerificationContentVerifying import io.element.android.features.verifysession.impl.ui.VerificationContentVerifying
import io.element.android.libraries.architecture.AsyncData 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.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.components.BigIcon 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.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@ -180,11 +186,26 @@ private fun OutgoingVerificationHeader(step: Step, request: VerificationRequest.
} }
is Step.Exit -> return is Step.Exit -> return
} }
val timeLimitMessage = if (step.isTimeLimited) {
PageTitle( 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, iconStyle = iconStyle,
title = stringResource(id = titleTextId), 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.Modifier
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource 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.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -39,14 +41,20 @@ internal fun VerificationContentVerifying(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Box( Box(
modifier = modifier.fillMaxSize().padding(bottom = 20.dp), modifier = modifier
.fillMaxSize()
.padding(bottom = 20.dp),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
when (data) { when (data) {
is SessionVerificationData.Decimals -> { is SessionVerificationData.Decimals -> {
val text = data.decimals.joinToString(separator = " - ") { it.toString() } val text = data.decimals.joinToString(separator = " - ")
Text( Text(
modifier = Modifier.fillMaxWidth(), modifier = Modifier
.fillMaxWidth()
.semantics {
contentDescription = data.decimals.joinToString()
},
text = text, text = text,
style = ElementTheme.typography.fontHeadingLgBold, style = ElementTheme.typography.fontHeadingLgBold,
color = ElementTheme.colors.textPrimary, color = ElementTheme.colors.textPrimary,
@ -57,7 +65,9 @@ internal fun VerificationContentVerifying(
// We want each row to have up to 4 emojis // We want each row to have up to 4 emojis
val rows = data.emojis.chunked(4) val rows = data.emojis.chunked(4)
Column( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier
.fillMaxWidth()
.semantics(mergeDescendants = true) {},
verticalArrangement = Arrangement.spacedBy(40.dp), verticalArrangement = Arrangement.spacedBy(40.dp),
) { ) {
rows.forEach { emojis -> 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.foundation.layout.Column
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier 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.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.theme.components.Text 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 @Composable
fun TextWithLabelMolecule( fun TextWithLabelMolecule(
label: String, label: String,
text: String, text: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
spellText: Boolean = false,
) { ) {
Column(modifier = modifier) { Column(modifier = modifier) {
Text( Text(
@ -26,6 +38,11 @@ fun TextWithLabelMolecule(
color = ElementTheme.colors.textSecondary, color = ElementTheme.colors.textSecondary,
) )
Text( Text(
modifier = Modifier.semantics {
if (spellText) {
contentDescription = text.toList().joinToString()
}
},
text = text, text = text,
style = ElementTheme.typography.fontBodyMdRegular, style = ElementTheme.typography.fontBodyMdRegular,
color = ElementTheme.colors.textPrimary, 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.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons 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.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.BigIcon
import io.element.android.libraries.designsystem.components.PageTitle
import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@ -69,9 +69,10 @@ fun FlowStepPage(
) )
}, },
header = { header = {
PageTitle( IconTitleSubtitleMolecule(
modifier = Modifier.padding(bottom = 16.dp),
title = title, title = title,
subtitle = subTitle, subTitle = subTitle,
iconStyle = iconStyle, 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.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@ -28,8 +30,8 @@ import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons 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.BigIcon
import io.element.android.libraries.designsystem.components.PageTitle
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Button
@ -57,14 +59,15 @@ fun MediaDeleteConfirmationBottomSheet(
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),
) { ) {
PageTitle( IconTitleSubtitleMolecule(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 16.dp, horizontal = 8.dp), .padding(vertical = 16.dp, horizontal = 8.dp),
title = stringResource(R.string.screen_media_browser_delete_confirmation_title), title = stringResource(R.string.screen_media_browser_delete_confirmation_title),
iconStyle = BigIcon.Style.Default(CompoundIcons.Delete(), useCriticalTint = true), 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( MediaRow(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .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.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter 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.background.OnboardingBackground
import io.element.android.libraries.designsystem.components.BigIcon 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.async.AsyncFailure
import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
@ -472,14 +472,14 @@ private fun EmptyContent(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) { ) {
OnboardingBackground() OnboardingBackground()
PageTitle( IconTitleSubtitleMolecule(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = 44.dp) .padding(top = 44.dp)
.padding(24.dp), .padding(24.dp),
title = stringResource(titleRes), title = stringResource(titleRes),
iconStyle = BigIcon.Style.Default(icon), iconStyle = BigIcon.Style.Default(icon),
subtitle = stringResource(subtitleRes), subTitle = stringResource(subtitleRes),
) )
} }
} }

View file

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

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9ea4fd71bc94982578bc061607066dfd3efe55aeccbec69b6826fd345113f95e
size 13296

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:56aabf6f359235c77b7bffa3d7e2c4ebf7b2c688eaf9e2d8a67da9a00433ebcb
size 12350

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:058f539d2f7652342394adb681084147dcc2d4a1e24f9c8efd47781a0def4c33
size 12925

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:315e1d831a1e3082a9d4ea749b76b374b0f8018c6d5c11b1f48ed34db3b3a1c5
size 13279

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:eb1c80dfc5926f3aa29ab5118f8c5a6a62ab13a1247e198e32f00509723cd4e3
size 12550

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:61c83e013e73d55a6b3504f34dcdb0111744499172839a71c35ada67cece1184
size 13162

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d81aade2945be977896038addacdb9246d17bb1a57fb3bd0d43169e382ce9f03
size 13126

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:46639efc4726cfb78ba5955c7c56619ce06342ef222c4c06eb8829c22d90e6f7
size 13200

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:316d70aa6507a9b0c5ab9833d750033df44f8197ca3f25d7f6acbfc496b68d59
size 12184

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d07c5029da082dcce7b86a202ea611e089ddb1e379d2511395f75dfabdedd5d3
size 13034

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:da1945462f70133c47e33eaaa9dfc3d64033c6a83d4d50b03776adcc7a7f77e0
size 13334

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d4871583b442845d3ae8e3dd98f4099fb7906f2ccccf25b61aad59efb8112c62
size 12259

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0d2d9b68cff6f00bf12eed5df4123af7da908ae3e25e710e5ce305f36b404382
size 13101

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:610e68016011b14db56217ebcd27a86ab597c1668a3a90b09c3e39ac308f8764
size 13054

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a1638d8805c8029573b8cae9a71c3c2dddf569d5154969f49085def60ec74ec8
size 7545

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:19c89df3b96fad54d28fdd608cec70d2d8c71fb20901746abacd9bcc26fd5332
size 7579